Merge branch 'master' of github.com:wagnerf42/BangleApps

pull/3205/head
frederic wagner 2023-12-08 17:53:14 +01:00
commit 371345b861
273 changed files with 6480 additions and 1903 deletions

View File

@ -278,6 +278,7 @@ and which gives information about the app for the Launcher.
// 'bluetooth' - uses Bluetooth LE
// 'system' - used by the system
// 'clkinfo' - provides or uses clock_info module for data on your clock face or clocks that support it (see apps/clock_info/README.md)
// 'health' - e.g. heart rate monitors or step counting
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID

View File

@ -84,6 +84,7 @@
<label class="chip tooltip" filterid="outdoors" data-tooltip="For outdoor use">Outdoors</label>
<label class="chip tooltip" filterid="ram" data-tooltip="Apps that don't save anything to flash memory">Online</label>
<label class="chip tooltip" filterid="clkinfo" data-tooltip="Info displayed on clocks, or clocks with info">Clock Info</label>
<label class="chip tooltip" filterid="health" data-tooltip="Apps for your health">Health</label>
<label class="chip tooltip" filterid="favourites" data-tooltip="Apps that you've liked ❤️">Favourites</label>
</div>
<div class="sort-nav hidden">
@ -176,9 +177,13 @@
</label>
<label class="form-switch">
<input type="checkbox" id="settings-alwaysAllowUpdate">
<i class="form-icon"></i> Always allow to reinstall apps in place regardless of the version
<i class="form-icon"></i> Always show "reinstall app" button <i class="icon icon-refresh"></i> regardless of the version
</label>
<button class="btn" id="defaultsettings">Reset to default App Loader settings</button>
<label class="form-switch">
<input type="checkbox" id="settings-autoReload">
<i class="form-icon"></i> Automatically reload watch after app App Loader actions (removes "Hold button" prompt)
</label>
<button class="btn" id="defaultsettings">Reset App Loader settings to defaults</button>
</details>
</div>
<div id="more-deviceinfo" style="display:none">

1
apps/Tyreid/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Change log created

18
apps/Tyreid/README.md Normal file
View File

@ -0,0 +1,18 @@
Tyreid
Tyreid is a Bluetooth war-driving app for the Bangle.js 2.
Menu options:
- Start: This turns on the Bluetooth and starts logging Bluetooth packets with time, latitude, and longitude information to a CSV file.
- Pause/Continue: These functions pause the capture and then allow it to resume.
- Devices: When paused this menu option will display the MAC addresses of discovered Bluetooth devices. Selecting a device will then display the MAC, Manufacturer code, the time it was first seen, and the RSSI of the first sighting.
- Marker: This command adds a 'marker' to the CSV log, which consists of the time and location information, but the Bluetooth packet information is replaced with the word MARKER. Markers can also be added by pressing the watch's button.
- Exit: This exits the app.
The current number of discovered devices is displayed in the top left corner.
This value is displayed in green when the GPS has a fix, or red otherwise.
To retrieve the CSV file, connect to the watch through the Espruino web IDE (https://www.espruino.com/ide/). From there the files stored on the watch can be downloaded by clicking the storage icon in the IDE's central column.

1
apps/Tyreid/app-icon.js Normal file
View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAtAAAAAAAAAAAAAAB/QAAAAAAAAAAAAAD/0AAAAAAAAAAAAAH+9AAAAAAAAAAAAAPtfQAAAAAAAAAAAAudH0AAAAAAAAAAAB8tB9AAAAAAAAAAAD0tAfQAAAAAAAAAAHgtAH0AAAAAAAAAAPAtAB9AAAAAAAAAAtAtAAfQAAAAAAAAB8AtAAH0AAAAAAAAD0AtAAB9AAAAAAAALgAtAAAfQAAAAAAAfAAtAAAH0AAAAAAA9AAtAAAB9AAAAAAC4AAtAAAAfQAAAAADwAAtAAAALwAAAAAAQAAtAAAAvgAAAAAAAAAsAAAC9AAAAAAAAAAsAAAP0AAAAAAAAAAsAAB/AAAAAAAAAAAsAAH4AAAAAAAAAAAsAAvQAAAAAAAAAAAsAD9AAAAAAAAAAAAsAD0AAAAAAAAAAAAsAB8AAAAAAAAAAAAsAAtAAAAAAAAAAAAsAAPQAAAAAAAAAAAsAAHwAAAAAAAAAAAsAAC4AAAAAAAAAAAsAAA9AAAAAAAAAAAsAAAPAAAAAAAAAAAsAAAHgAAAAAAAAAA8AAAC0AAAAAAAAAA8AAAA8AAAAAAAAAA8AAAAEAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))

272
apps/Tyreid/app.js Normal file

File diff suppressed because one or more lines are too long

14
apps/Tyreid/metadata.json Normal file
View File

@ -0,0 +1,14 @@
{ "id": "Tyreid",
"name": "Tyreid",
"shortName":"Tyreid",
"version":"0.01",
"description": "Bluetooth war-driving app for Bangle.js 2",
"icon": "small_logo.png",
"tags": "",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"Tyreid.app.js","url":"app.js"},
{"name":"Tyreid.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/Tyreid/small_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -45,4 +45,7 @@
0.40: Use substring of message when it's longer than fits the designated menu entry.
0.41: Fix a menu bug affecting alarms with empty messages.
0.42: Fix date not getting saved in event edit menu when tapping Confirm
0.43: New settings: Show confirm, Show Overflow, Show Type.
0.43: New settings: Show confirm, Show Overflow, Show Group.
0.44: Add "delete timer after expiration" setting to events.
0.45: Fix new alarm when selectedAlarm is undefined
0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it.

View File

@ -73,39 +73,57 @@ function formatAlarmProperty(msg) {
}
}
function showMainMenu() {
function showMainMenu(scroll, group) {
const menu = {
"": { "title": /*LANG*/"Alarms & Timers" },
"< Back": () => load(),
/*LANG*/"New...": () => showNewMenu()
"": { "title": group || /*LANG*/"Alarms & Timers", scroll: scroll },
"< Back": () => group ? showMainMenu() : load(),
/*LANG*/"New...": () => showNewMenu(group)
};
const getGroups = settings.showGroup && !group;
const groups = getGroups ? {} : undefined;
var showAlarm;
alarms.forEach((e, index) => {
showAlarm = !settings.showGroup || (group ? e.group === group : !e.group);
if(showAlarm) {
menu[trimLabel(getLabel(e),40)] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller.scroll, group)
};
} else if (getGroups) {
groups[e.group] = undefined;
}
});
if (!group) {
Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g));
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
E.showMenu(menu);
}
function showNewMenu() {
E.showMenu({
var scroller = E.showMenu(menu).scroller;
}
function showNewMenu(group) {
const newMenu = {
"": { "title": /*LANG*/"New..." },
"< Back": () => showMainMenu(),
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
"< Back": () => showMainMenu(group),
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined, false, null, group),
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined),
/*LANG*/"Event": () => showEditAlarmMenu(undefined, undefined, true)
});
/*LANG*/"Event": () => showEditAlarmMenu(undefined, undefined, true, null, group)
};
if (group) delete newMenu[/*LANG*/"Timer"];
E.showMenu(newMenu);
}
function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) {
var isNew = alarmIndex === undefined;
var alarm = require("sched").newDefaultAlarm();
if (isNew && group) alarm.group = group;
if (withDate || (selectedAlarm && selectedAlarm.date)) {
alarm.del = require("sched").getSettings().defaultDeleteExpiredTimers;
}
alarm.dow = handleFirstDayOfWeek(alarm.dow);
if (selectedAlarm) {
@ -124,7 +142,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
"< Back": () => {
prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload();
showMainMenu();
showMainMenu(scroll, group);
},
/*LANG*/"Hour": {
value: time.h,
@ -168,7 +186,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
keyboard.input({text:alarm.msg}).then(result => {
alarm.msg = result;
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group);
});
}, 100);
}
@ -181,7 +199,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
keyboard.input({text:alarm.group}).then(result => {
alarm.group = result;
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group);
});
}, 100);
}
@ -193,10 +211,13 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
/*LANG*/"Repeat": {
value: decodeRepeat(alarm),
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, date || alarm.dow, (repeat, dow) => {
if (repeat) {
alarm.del = false; // do not auto delete a repeated alarm
}
alarm.rp = repeat;
alarm.dow = dow;
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group);
})
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
@ -204,15 +225,19 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
value: alarm.as,
onchange: v => alarm.as = v
},
/*LANG*/"Delete After Expiration": {
value: alarm.del,
onchange: v => alarm.del = v
},
/*LANG*/"Hidden": {
value: alarm.hidden || false,
onchange: v => alarm.hidden = v
},
/*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Cancel": () => showMainMenu(scroll, group),
/*LANG*/"Confirm": () => {
prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload();
showMainMenu();
showMainMenu(scroll, group);
}
};
@ -225,6 +250,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
delete menu[/*LANG*/"Day"];
delete menu[/*LANG*/"Month"];
delete menu[/*LANG*/"Year"];
delete menu[/*LANG*/"Delete After Expiration"];
}
if (!isNew) {
@ -233,10 +259,10 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
if (confirm) {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
showMainMenu(scroll, group);
} else {
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group);
}
});
};
@ -283,7 +309,6 @@ function decodeRepeat(alarm) {
}
function showEditRepeatMenu(repeat, day, dowChangeCallback) {
var originalRepeat = repeat;
var dow;
const menu = {
@ -316,26 +341,32 @@ function showEditRepeatMenu(repeat, day, dowChangeCallback) {
},
/*LANG*/"Custom": {
value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false,
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow)
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, repeat, originalDow)
}
};
} else {
// var date = day; // eventually: detect day of date and configure a repeat e.g. 3rd Monday of Month
dow = EVERY_DAY;
repeat = repeat || {interval: "month", num: 1};
const repeatObj = repeat || {interval: "month", num: 1};
restOfMenu = {
/*LANG*/"Every": {
value: repeat.num,
value: repeatObj.num,
min: 1,
onchange: v => repeat.num = v
onchange: v => {
repeat = repeatObj;
repeat.num = v;
}
},
/*LANG*/"Interval": {
value: INTERVALS.indexOf(repeat.interval),
value: INTERVALS.indexOf(repeatObj.interval),
format: v => INTERVAL_LABELS[v],
min: 0,
max: INTERVALS.length - 1,
onchange: v => repeat.interval = INTERVALS[v]
onchange: v => {
repeat = repeatObj;
repeat.interval = INTERVALS[v];
}
}
};
}

View File

@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.43",
"version": "0.46",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm",

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Actually upload correct code
0.03: Display sea-level pressure, too, and allow calibration
0.04: Switch to using system code for pressure calibration

View File

@ -3,59 +3,54 @@ Bangle.setBarometerPower(true, "altimeter");
g.clear(1);
Bangle.loadWidgets();
Bangle.drawWidgets();
var zero = 0;
var R = Bangle.appRect;
var y = R.y + R.h/2;
var MEDIANLENGTH = 20;
var avr = [], median;
var value = 0;
var avr = [];
function getStandardPressure(altitude) {
const P0 = 1013.25; // standard pressure at sea level in hPa
const T0 = 288.15; // standard temperature at sea level in K
const g0 = 9.80665; // standard gravitational acceleration in m/s^2
const R = 8.31432; // gas constant in J/(mol*K)
const M = 0.0289644; // molar mass of air in kg/mol
const L = -0.0065; // temperature lapse rate in K/m
const temperature = T0 + L * altitude; // temperature at the given altitude
const pressure = P0 * Math.pow((temperature / T0), (-g0 * M) / (R * L)); // pressure at the given altitude
return pressure;
}
function convertToSeaLevelPressure(pressure, altitude) {
return 1013.25 * (pressure / getStandardPressure(altitude));
function fmt(t) {
if ((t > -100) && (t < 1000))
t = t.toFixed(1);
else
t = t.toFixed(0);
return t;
}
Bangle.on('pressure', function(e) {
while (avr.length>MEDIANLENGTH) avr.pop();
avr.unshift(e.altitude);
median = avr.slice().sort();
let median = avr.slice().sort();
g.reset().clearRect(0,y-30,g.getWidth()-10,R.h);
if (median.length>10) {
var mid = median.length>>1;
value = E.sum(median.slice(mid-4,mid+5)) / 9;
t = value-zero;
if ((t > -100) && (t < 1000))
t = t.toFixed(1);
else
t = t.toFixed(0);
var value = E.sum(median.slice(mid-4,mid+5)) / 9;
} else {
var value = median[median.length>>1];
}
t = fmt(value);
g.setFont("Vector",50).setFontAlign(0,0).drawString(t, g.getWidth()/2, y);
sea = convertToSeaLevelPressure(e.pressure, value-zero);
let o = Bangle.getOptions();
let sea = o.seaLevelPressure;
t = sea.toFixed(1) + " " + e.temperature.toFixed(1);
if (0) {
print("alt raw:", value.toFixed(1));
print("temperature:", e.temperature);
print("pressure:", e.pressure);
print("sea pressure:", sea);
print("std pressure:", getStandardPressure(value-zero));
}
g.setFont("Vector",25).setFontAlign(-1,0).drawString(t,
10, R.y+R.h - 35);
}
g.setFont("Vector",25).setFontAlign(-1,0).drawString(t, 10, R.y+R.h - 35);
});
function setPressure(m, a) {
o = Bangle.getOptions();
print(o);
o.seaLevelPressure = o.seaLevelPressure * m + a;
Bangle.setOptions(o);
avr = [];
}
print(g.getFonts());
g.reset();
g.setFont("Vector:15");
@ -63,9 +58,9 @@ g.setFontAlign(0,0);
g.drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40);
g.drawString(/*LANG*/"SEA L (hPa) TEMP (C)", g.getWidth()/2, y+62);
g.flip();
g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"ZERO", g.getWidth()-5, g.getHeight()/2);
g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"STD", g.getWidth()-5, g.getHeight()/2);
Bangle.setUI("updown", btn=> {
if (!btn) zero=value;
if (btn<0) zero-=5;
if (btn>0) zero+=5;
if (!btn) setPressure(0, 1013.25);
if (btn<0) setPressure(1, 1);
if (btn>0) setPressure(1, -1);
});

View File

@ -1,6 +1,6 @@
{ "id": "altimeter",
"name": "Altimeter",
"version":"0.03",
"version":"0.04",
"description": "Simple altimeter that can display height changed using Bangle.js 2's built in pressure sensor.",
"icon": "app.png",
"tags": "tool,outdoors",

View File

@ -0,0 +1,4 @@
0.1: Initial release
0.2: Added more descriptive approximations
0.2f: Bug fixes: Incorrect hour drawn after 50 mins, incorrect quarter minute drawn after 50 mins
0.3: Added touch interaction to display exact time and date.

View File

@ -0,0 +1 @@
atob("MDAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArgVYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABW19cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrNcrAACBVoGsVgAAgVaBrFYAAKzXK4GsKwAAgayBAAAAgVYAVoEAAAAAAAAAAADXVqyBAACs14Gs1ysArNeBrNcrAIHX14HXgQCB14HXrAAAVtdW11YAAAAAAAAAAFbXK4GsAACs1wAr11YArNcAK9dWAADXrABWVgDXgQCB1wAAAKzXgQAAAAAAAAAAAKzXrNfXKwCs1wAA14EArKwAK9dWAADXVgAAAADXgQBW1ysAAIHXVgAAAAAAAAAAANfXgYHXVgCs11aB11YArNcrgddWACvXgSsAAACsrCus1wAAK9es1ysAAAAAAAAAK9dWAACsrACsrKzXrAAArKzX16wAVtfX16wAAAAr19fXKwAArKwArKwAAAAAAAAAAAAAAAAAAACsrAArAAAArIEAKwAAAAAAAAAAAAAAACsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrAAAAAAArIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWVgAAAAAAVlYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArVoErAAAAAAAAAAAAAAAAAAAAAAArVgAAAAAAAAAAAAAAAAAAAAAAAAArrNfXgQCBrNdWAAAAAAAAAAAAAAAAAAAAAABW1wAAAAAAAAAAAAAAAAAAAAAAACvXrCtWgQAAANdWAAAAAAArKwAAAAAAACsAAABW1wAAAAAAAAAAAAAAAAAAAAAAAFbXKwAAAAAAANdWAAAAAIHX16wAAACB19fXVgBW1wAA14EAAAAAAAAAAAAAAAAAAIGsAAAAAAAAANdWAAAAK9eBVteBACvXgSuBVgBW1wBW1ysAAAAAAAAAAAAAAAAAAIHXAAAAAAAAANdWAAAAVtcrAKyBAFbXKwAAAABW16zX1wAAAAAAAAAAAAAAAAAAAFbXVgAAAAAAANeBAAAAVtcrANeBAFbXKwAAAABW14HXrAAAAAAAAAAAAAAAAAAAAACs14GBrAAAAKzXrIEAK9esrNdWACvX14GBVgBW1wAr11YAAAAAAAAAAAAAAAAAAAAAVoGBgQAAACusrIEAACusrFYAAAArgaysVgBWgQAAgYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")

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

@ -0,0 +1,156 @@
//load fonts
require("FontSinclair").add(Graphics);
require("FontTeletext5x9Ascii").add(Graphics);
//const
const numbers = {
"0": "Twelve",
"1": "One",
"2": "Two",
"3": "Three",
"4": "Four",
"5": "Five",
"6": "Six",
"7": "Seven",
"8": "Eight",
"9": "Nine",
"10": "Ten",
"11": "Eleven",
"12": "Twelve",
"13": "One",
"14": "Two",
"15": "Three",
"16": "Four",
"17": "Five",
"18": "Six",
"19": "Seven",
"20": "Eight",
"21": "Nine",
"22": "Ten",
"23": "Eleven",
"24": "Twelve",
};
const minutesByQuarterString = {
0: "O'Clock",
15: "Fifteen",
30: "Thirty",
45: "Fourty-Five"
};
const width = g.getWidth();
const height = g.getHeight();
let drawTimeout;
const getNearestHour = (hours, minutes) => {
if (minutes > 54) {
return hours + 1;
}
return hours;
};
const getApproximatePrefix = (minutes, minutesByQuarter) => {
if (minutes === minutesByQuarter) {
return " exactly";
} else if (minutesByQuarter - minutes < -54) {
return " nearly";
} else if (minutesByQuarter - minutes < -5) {
return " after";
} else if (minutesByQuarter - minutes < 0) {
return " just after";
} else if (minutesByQuarter - minutes > 5) {
return " before";
} else {
return " nearly";
}
};
const getMinutesByQuarter = minutes => {
if (minutes < 10) {
return 0;
} else if (minutes < 20) {
return 15;
} else if (minutes < 40) {
return 30;
} else if (minutes < 55) {
return 45;
} else {
return 0;
}
};
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function () {
drawTimeout = undefined;
drawTime();
}, 60000 - (Date.now() % 60000));
}
const drawTimeExact = () => {
var dateTime = Date();
var hours = dateTime.getHours();
var minutes = dateTime.getMinutes().toString().padStart(2,0);
var day = dateTime.getDay();
var date = dateTime.getDate();
var month = dateTime.getMonth();
var year = dateTime.getFullYear();
g.clear();
g.setBgColor(0,0,0);
g.clearRect(0,0,width, height);
g.setColor(1,1,1);
g.setFont("Vector", 30);
g.drawString(hours + ":" + minutes, (width - g.stringWidth(hours + ":" + minutes))/2, height * 0.3, false);
g.setFont("Vector", 26);
g.drawString(month + 1 + "/" + date + "/" + year, (width - g.stringWidth(month + 1 + "/" + date + "/" + year))/2, height * 0.6, false);
};
const drawTime = () => {
//Grab time vars
var date = Date();
var hour = date.getHours();
var minutes = date.getMinutes();
var minutesByQuarter = getMinutesByQuarter(minutes);
//reset graphics
g.clear();
g.reset();
//Build watch face
g.setBgColor(0, 0, 0);
g.clearRect(0, 0, width, height);
g.setFont("Vector", 22);
g.setColor(1, 1, 1);
g.drawString("It's" + getApproximatePrefix(minutes, minutesByQuarter), (width - g.stringWidth("It's" + getApproximatePrefix(minutes, minutesByQuarter))) / 2, height * 0.25, false);
g.setFont("Vector", 30);
g.drawString(numbers[getNearestHour(hour, minutes)], (width - g.stringWidth(numbers[getNearestHour(hour, minutes)])) / 2, height * 0.45, false);
g.setFont("Vector", 22);
g.drawString(minutesByQuarterString[minutesByQuarter], (width - g.stringWidth(minutesByQuarterString[minutesByQuarter])) / 2, height * 0.7, false);
queueDraw();
};
g.clear();
drawTime();
Bangle.on('lcdPower', function (on) {
if (on) {
drawTime();
} else {
if (idTimeout) {
clearTimeout(idTimeout);
}
}
});
Bangle.on('touch', function(button, xy){
drawTimeExact();
setTimeout(drawTime, 7000);
});
// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/approxclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,18 @@
{ "id": "approxclock",
"name": "Approximate Clock",
"shortName" : "Approx Clock",
"version": "0.3",
"icon": "app.png",
"description": "A really basic spelled out time display for people looking for the vague time at a glance.",
"readme": "readme.md",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"approxclock.app.js","url":"app.js"},
{"name":"approxclock.img","url":"app-icon.js","evaluate":true}
],
"screenshots": [
{"url": "screenshot.png"}
]
}

View File

@ -0,0 +1,7 @@
## Approximate Clock
### Description
Get a rough idea of the time at a quick glance, mostly made for myself based on a similar watchface on pebble. I find this keeps me from checking my watch too often and also saves me from moments of severe brainfart staring at these mysterious symbols we call numbers.
Exact time and date can be viewed temporarily by touching the screen.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -3,3 +3,6 @@
0.03: Update to use Bangle.setUI instead of setWatch
0.04: Tell clock widgets to hide.
0.05: Added adjustment for Bangle.js magnetometer heading fix
0.06: optimized to update much faster
0.07: added support for bangle.js 2
0.08: call setUI before loading widgets to indicate we're a clock

View File

@ -1,5 +1,7 @@
Astral Clock
============
NOTE FOR THE BANGLE 2 THIS APP ONLY SUPPORTS USING THE BLACK BACKGROUND CURRENTLY
Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting.
![screenshot](./Example.PNG)
@ -7,23 +9,21 @@ Clock that calculates and displays Alt Az positions of all planets, Sun as well
Functions
---------
**BTN1**: Refreshes Alt/Az readings. The coordinates are NOT continually updated, this is to save resources and battery usage plus it avoids you having to wait for calculations to finish before you can do anything else on the watch - it doesn't take long but it could still be annoying.
**BTN2**: Load side-menu as standard for clocks.
**BTN3**: Changes between planet mode and extra/other targets - discussed below (will still need to press button 1 after switching to update calcs).
**BTN4**: This is the left touchscreen, and when the LCD is on you can use this to change the font between red/white. This will only work after the GPS location has been set initially.
Swiping left or right will alternate between planets and other astronomy targets, see below for how to change these addtional ones.
The text will turn blue during calculation and then back again once complete.
The data is refreshed automatically every 2 minutes. You can force a refresh as well by swiping up or, on Bangle 1, pressing Button 3.
When you first install it, all positions will be estimated from UK as the default location and all the text will be white; from the moment you get your first GPS lock with the clock, it will save your location, recalculate accordingly and change the text to red, ideal for maintaining night vision, the calculations will also now be relevant to your location and time. If you have not used the GPS yet, I suggest using it outside briefly to get your first fix as the initial one can take a bit longer, although it should still just be a minute or 2 max normally.
Swiping down will disable/enable the compass and GPS.
When you first install it, all positions will be estimated from UK as the default location and all the text will be white; from the moment you get your first GPS lock with the clock, it will save your location, recalculate accordingly and change the text to red, ideal for maintaining night vision. One the Bangle.JS 2, the colour will be a light blue rather than red because the colours are not as vibrant. The calculations will also now be relevant to your location and time. If you have not used the GPS yet, I suggest using it outside briefly to get your first fix as the initial one can take a bit longer, although it should still just be a minute or 2 max normally.
Lat and Lon are saved in a file called **astral.config**. You can review this file if you want to confirm current coordinates or even hard set different values \- although be careful doing the latter as there is no error handling to manage bad values here so you would have to delete the file and have the app generate a new one if that happens, also the GPS functionality will overwrite anything you put in here once it picks up your location.
There can currently be a slight error mainly to the Az at times due to a firmware issue for acos (arccosine) that affect spherical calculations but I have used an estimator function that gives a good enough accuracy for general observation so shouldn't noticeably be too far off. I\'ll be implementing acos for better accuracy when the fix is in a standard release and the update will still include the current estimate function to support a level of backward compatibility.
The moon phases are split into the 8 phases with an image for each - new moon would show no image.
The compass is displayed above the minute digits, if you get strange values or dashes the compass needs calibration but you just need to move the watch around a bit 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 doing Wing Chun Kuen.
The compass is displayed on the left.
Also the compass isn\t tilt compensated so try and keep the face parallel when taking a reading.
Also the compass isn\t tilt compensated so try and keep the face parallel when taking a reading. It's more of an indicator, for a more accurate compass reading, you can use one of the many great apps in the apploader that compensated for movement and angles of the watch etc.
Additional Astronomy Targets
----------------------------
@ -37,7 +37,7 @@ The type property is not utilised as yet but relates to whether the object is (i
Updates & Feedback
------------------
Put together, initially at least, by \"Ben Jabituya\", https://jabituyaben.wixsite.com/majorinput, jabituyaben@gmail.com. Feel free to get in touch for any feature request. Also I\'m not precious at all - if you know of efficiencies or improvements you could make, just put the changes in. One thing that would probably be ideal is to change some of the functions to inline C to make it faster.
Put together, initially at least, by \"Ben Jabituya\", https://majorinput.co.uk, jabituyaben@gmail.com.
Credit to various sources from which I have literally taken source code and shoehorned to fit on the Bangle:

View File

@ -1,7 +1,11 @@
setupcomplete_colour = "#ff3329";
default_colour = "#ffffff";
calc_display_colour = "#00FFFF";
display_colour = default_colour;
require("Font8x12").add(Graphics);
require("Font7x11Numeric7Seg").add(Graphics);
var setupcomplete_colour = "#ff3329";
var default_colour = "#ffffff";
var calc_display_colour = "#00FFFF";
var display_colour = default_colour;
var processing = false;
var all_extras_array = [];
@ -10,11 +14,19 @@ var mode = "planetary";
var modeswitch = false;
var colours_switched = false;
var sensorsOn = false;
// Load fonts
require("Font7x11Numeric7Seg").add(Graphics);
// position on screen
const Xaxis = 150, Yaxis = 55;
var screenSize = g.getHeight();
console.log(screenSize);
var Xaxis = 150, Yaxis = 55;
if (screenSize <= 176) {
setupcomplete_colour = "#00FFFF";
Xaxis = 110;
Yaxis = 40;
}
//lat lon settings loading
var astral_settings;
@ -443,7 +455,8 @@ function dec2str(x) {
var sgn = (x < 0) ? "-" : " ";
var d = Math.floor(dec);
var m = 60 * (dec - d);
return sgn + cintstr(d, 2) + "° " + frealstr(m, 4, 1) + "'";
//return sgn + cintstr(d, 2) + "° " + frealstr(m, 4, 1) + "'";
return sgn + cintstr(d, 2) + "° ";
}
// return the integer part of a number
@ -534,14 +547,7 @@ function mean_sidereal_time(lon) {
var mst = 280.46061837 + 360.98564736629 * jd
+ 0.000387933 * jt * jt - jt * jt * jt / 38710000 + lon;
if (mst > 0.0) {
while (mst > 360.0)
mst = mst - 360.0;
}
else {
while (mst < 0.0)
mst = mst + 360.0;
}
mst = mst % 360;
return mst;
}
@ -559,7 +565,8 @@ function degr2str(x) {
var sgn = (x < 0) ? "-" : " ";
var d = Math.floor(dec);
var m = 60 * (dec - d);
return sgn + cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'";
//return sgn + cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'";
return sgn + cintstr(d, 3) + "° ";
}
// converts latitude in signed degrees into string
@ -568,7 +575,8 @@ function lat2str(x) {
var sgn = (x < 0) ? " S" : " N";
var d = Math.floor(dec);
var m = 60 * (dec - d);
return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn;
//return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn;
return cintstr(d, 3) + "° ";
}
// converts longitude in signed degrees into string
@ -577,7 +585,8 @@ function lon2str(x) {
var sgn = (x < 0) ? " W" : " E";
var d = Math.floor(dec);
var m = 60 * (dec - d);
return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn;
//return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn;
return cintstr(d, 3) + "° ";
}
// format two digits with leading zero if needed
@ -649,9 +658,9 @@ function write_refresh_note(colour) {
if (!ready_to_compute) {
g.drawString("mode change:", Xaxis + 50, cursor, false);
cursor += 15;
g.drawString("BTN1 to refresh", Xaxis + 50, cursor, true /*clear background*/);
g.drawString("swipe up to refresh", Xaxis + 50, cursor, true /*clear background*/);
cursor += 15;
g.drawString("BTN3 to cancel", Xaxis + 50, cursor, true /*clear background*/);
g.drawString("swipe left/right to cancel", Xaxis + 50, cursor, true /*clear background*/);
}
else
g.drawString("updating, please wait", Xaxis + 50, cursor, false);
@ -659,42 +668,92 @@ function write_refresh_note(colour) {
function draw_moon(phase) {
g.setColor(display_colour);
var moonOffset = 12;
var mooonRadius = 25;
if (screenSize > 176) {
if (phase == 5) {
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
g.setColor("#000000");
g.fillRect(220, 25, 240, 90);
}
else if (phase == 6) {
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
g.setColor("#000000");
g.fillRect(200, 25, 240, 90);
}
else if (phase == 1) {
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
g.setColor("#000000");
g.fillCircle(180, Yaxis, 30);
g.fillCircle(180, Yaxis + moonOffset, mooonRadius);
}
else if (phase == 4)
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
else if (phase == 3) {
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
g.setColor("#000000");
g.fillRect(160, 25, 180, 90);
}
else if (phase == 2) {
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
g.setColor("#000000");
g.fillRect(160, 25, 200, 90);
}
else if (phase == 7) {
g.fillCircle(200, Yaxis, 30);
g.fillCircle(200, Yaxis + moonOffset, mooonRadius);
g.setColor("#000000");
g.fillCircle(220, Yaxis, 30);
g.fillCircle(220, Yaxis, 25);
}
}
else {
moonOffset = 12;
//var moonOffsetX = 150;
mooonRadius = 17;
g.setColor(display_colour);
if (phase != 0)
g.fillCircle(150, Yaxis + moonOffset, mooonRadius);
if (phase == 5) {
g.setColor("#000000");
g.fillRect(165, 25, 180, 90);
}
else if (phase == 6) {
g.setColor("#000000");
g.fillRect(150, 25, 240, 90);
}
else if (phase == 1) {
g.setColor("#000000");
g.fillCircle(140, Yaxis + moonOffset, mooonRadius);
}
else if (phase == 4)
g.fillCircle(150, Yaxis + moonOffset, mooonRadius);
else if (phase == 3) {
g.setColor("#000000");
g.fillRect(125, 25, 135, 90);
}
else if (phase == 2) {
g.setColor("#000000");
g.fillRect(125, 25, 150, 90);
}
else if (phase == 7) {
g.setColor("#000000");
g.fillCircle(160, Yaxis + moonOffset, mooonRadius);
}
}
g.setColor(display_colour);
}
function autoUpdate() {
ready_to_compute = true;
g.setColor(display_colour);
g.fillCircle(15, 160, 5);
setTimeout(function () {
print("ready");
draw();
secondInterval = setInterval(draw, 1000);
}, 100);
}
function draw() {
//print("drawing");
if (astral_settings.astral_default)
display_colour = default_colour;
else if (!colours_switched)
@ -708,19 +767,26 @@ function draw() {
g.reset();
g.setColor(display_colour);
// draw the current time (4x size 7 segment)
g.setFont("7x11Numeric7Seg", 5);
g.setFont("7x11Numeric7Seg", 4);
g.setFontAlign(1, 1); // align right bottom
g.drawString(time, Xaxis + 20, Yaxis + 30, true /*clear background*/);
g.drawString(time, Xaxis + 23, Yaxis + 34, true /*clear background*/);
g.setFont("6x8");
g.setFontAlign(1, 1); // align center bottom
// pad the date - this clears the background if the date were to change length
var dateStr = " " + require("locale").date(d) + " ";
g.drawString(dateStr, Xaxis - 40, Yaxis - 40, true /*clear background*/);
if (screenSize < 177) {
g.drawString(dateStr, 100, 20, true /*clear background*/);
}
else {
//bangle 1
g.drawString(dateStr, 150, 40, true /*clear background*/);
}
//compute location of objects
g.setFontAlign(1, 1);
g.setFont("6x8");
g.setFont("6x8", 1);
if (ready_to_compute)
g.setColor(calc_display_colour);
@ -729,25 +795,15 @@ function draw() {
g.setColor("#000000");
cursor = Yaxis + 50;
if (pstrings.length == 0) {
if (ready_to_compute)
g.drawString("updating, please wait", Xaxis + 50, cursor, true);
else
g.drawString("press BTN1 to update", Xaxis + 50, cursor, true /*clear background*/);
}
else {
if (pstrings.length != 0) {
for (let i = 0; i < pstrings.length; i++) {
g.drawString(pstrings[i], Xaxis + 50, cursor, true /*clear background*/);
cursor += 15;
g.drawString(pstrings[i], Xaxis + 40, cursor, true /*clear background*/);
cursor += 10;
}
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
}
if (modeswitch)
if (ready_to_compute)
write_refresh_note(calc_display_colour);
else
write_refresh_note(display_colour);
if (ready_to_compute) {
processing = true;
ready_to_compute = false;
@ -757,33 +813,44 @@ function draw() {
g.fillRect(Xaxis - 150, Yaxis + 40, Xaxis + 200, Yaxis + 200);
modeswitch = false;
processing = false;
Bangle.buzz();
//Bangle.buzz();
}
current_moonphase = getMoonPhase();
all_extras_array = [];
if (sensorsOn) {
g.setColor(display_colour);
g.fillCircle(160, 160, 5);
}
}
function SwitchSensorState() {
if (sensorsOn) {
print("turning sensors off");
Bangle.setCompassPower(0);
Bangle.setGPSPower(0);
sensorsOn = 0;
g.setColor("#000000");
g.fillCircle(screenSize - 15, screenSize - 15, 7);
//g.drawString(compass_heading, 30, 115, true /*clear background*/);
g.fillRect(0, 100, 30, 120);
}
else {
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
sensorsOn = 1;
g.setColor(display_colour);
g.fillCircle(screenSize - 15, screenSize - 15, 5);
}
}
g.clear();
g.setColor("#000000");
g.setBgColor(0, 0, 0);
g.fillRect(0, 0, 175, 175);
current_moonphase = getMoonPhase();
Bangle.setUI("clockupdown", btn => {
if (btn==0) {
if (!processing) {
if (!modeswitch) {
modeswitch = true;
if (mode == "planetary") mode = "extras";
else mode = "planetary";
}
else
modeswitch = false;
}
} else {
if (!processing)
ready_to_compute = true;
}
});
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
@ -792,59 +859,70 @@ Bangle.drawWidgets();
draw_moon(current_moonphase);
draw();
var secondInterval = setInterval(draw, 1000);
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', on => {
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
Bangle.setCompassPower(0);
if (!astral_settings.astral_default)
Bangle.setGPSPower(0);
if (on) {
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
if (current_moonphase !== undefined) {
draw_moon(current_moonphase);
}
secondInterval = setInterval(draw, 1000);
draw(); // draw immediately
}
});
var updateInterval = setInterval(autoUpdate, 120000);
//var magInterval = setInterval(updateMag, 50);
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
var secondInterval;
autoUpdate();
setWatch(SwitchSensorState, BTN1, { repeat: true });
if(process.env.HWVERSION != 2)
setWatch(autoUpdate, BTN3, { repeat: true });
// Show launcher when button pressed
Bangle.setClockMode();
//Bangle.setClockMode();
setWatch(function () {
if (!astral_settings.astral_default) {
colours_switched = true;
if (display_colour == setupcomplete_colour)
display_colour = default_colour;
else
display_colour = setupcomplete_colour;
draw_moon(current_moonphase);
Bangle.on("swipe", function (directionLR, directionUD) {
if (!processing) {
if (-1 == directionUD) {
g.setColor(display_colour);
g.fillCircle(15, 160, 5);
ready_to_compute = true;
}
else if (-1 == directionLR || directionLR == 1) {
print("attempting mode switch");
if (mode == "planetary") mode = "extras";
else mode = "planetary";
g.setColor(display_colour);
g.fillCircle(15, 160, 5);
ready_to_compute = true;
}
else if (directionUD == 1) {
SwitchSensorState();
}
}, BTN4, { repeat: true });
setTimeout(function () {
print("ready");
draw();
secondInterval = setInterval(draw, 1000);
}, 100);
}
});
//events
Bangle.on('mag', function (m) {
g.setFont("6x8",2);
if (isNaN(m.heading))
compass_heading = "---";
else
if (!isNaN(m.heading)) {
compass_heading = Math.round(m.heading);
// g.setColor("#000000");
// g.fillRect(160, 10, 160, 20);
g.setColor(display_colour);
if (compass_heading < 100)
compass_heading = " " + compass_heading;
g.drawString(compass_heading, 150, 20, true /*clear background*/);
if (sensorsOn) {
g.setFont("8x12");
g.setFontAlign(1, 1);
g.drawString(compass_heading, 25, 112, true /*clear background*/);
}
}
//var n = magArray.length;
//var mean = Math.round( magArray.reduce((a, b) => a + b) / n);
//compass_heading = mean;
});
Bangle.on('GPS', function (g) {
if (g.fix) {
display_colour = setupcomplete_colour;
astral_settings.lat = g.lat;
astral_settings.lon = g.lon;
astral_settings.astral_default = false;

View File

@ -1,13 +1,13 @@
{
"id": "astral",
"name": "Astral Clock",
"version": "0.05",
"version": "0.08",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"readme": "README.md",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"astral.app.js","url":"app.js"},
{"name":"astral.img","url":"app-icon.js","evaluate":true}

View File

@ -1,2 +1,3 @@
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.

View File

@ -26,7 +26,7 @@
if (Bangle["#on"+eventType] === undefined) {
return 0;
} else if (Bangle["#on"+eventType] instanceof Array) {
return Bangle["#on"+eventType].length;
return Bangle["#on"+eventType].filter(x=>x).length;
} else if (Bangle["#on"+eventType] !== undefined) {
return 1;
}

View File

@ -1,7 +1,7 @@
{ "id": "backswipe",
"name": "Back Swipe",
"shortName":"BackSwipe",
"version":"0.02",
"version":"0.03",
"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

@ -4,3 +4,4 @@
0.04: Improve current time readability in light theme.
0.05: Show calendar colors & improved all day events.
0.06: Improved multi-line locations & titles
0.07: Buzz 30, 15 and 1 minute before an event

View File

@ -117,6 +117,17 @@ function fullRedraw() {
drawFutureEvents(y);
}
function buzzForEvents() {
let nextEvent = next[0]; if (!nextEvent) return;
if (nextEvent.allDay) return;
let minToEvent = Math.round((nextEvent.timestamp - getTime()) / 60.0);
switch (minToEvent) {
case 30: require("buzz").pattern(","); break;
case 15: require("buzz").pattern(", ,"); break;
case 1: require("buzz").pattern(": : :"); break;
}
}
function redraw() {
g.reset();
if (current.find(e=>!isActive(e)) || next.find(isActive)) {
@ -124,10 +135,12 @@ function redraw() {
} else {
drawCurrentEvents(30);
}
buzzForEvents();
}
g.clear();
fullRedraw();
buzzForEvents();
var minuteInterval = setInterval(redraw, 60 * 1000);
Bangle.setUI("clock");

View File

@ -2,7 +2,7 @@
"id": "calclock",
"name": "Calendar Clock",
"shortName": "CalClock",
"version": "0.06",
"version": "0.07",
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png",
"type": "clock",

View File

@ -15,3 +15,5 @@
Display events for current month on touch
0.14: Add support for holidays
0.15: Edit holidays on device in settings
0.16: Add menu to fast open settings to edit holidays
Display Widgets in menus

View File

@ -1,6 +1,6 @@
# Calendar
Basic calendar
Monthly calendar, displays holidays uploaded from the web interface and scheduled events.
## Usage
@ -14,4 +14,4 @@ Basic calendar
## Settings
- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.
B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme.

View File

@ -1,3 +1,4 @@
{
const maxX = g.getWidth();
const maxY = g.getHeight();
const fontSize = g.getWidth() > 200 ? 2 : 1;
@ -17,22 +18,33 @@ const red = "#d41706";
const blue = "#0000ff";
const yellow = "#ffff00";
const cyan = "#00ffff";
let bgColor = color4;
let bgColorMonth = color1;
let bgColorDow = color2;
let bgColorWeekend = color3;
let fgOtherMonth = gray1;
let fgSameMonth = white;
let bgEvent = blue;
let bgOtherEvent = "#ff8800";
let bgColor;
let bgColorMonth;
let bgColorDow;
let bgColorWeekend;
let fgOtherMonth;
let fgSameMonth;
let bgEvent;
let bgOtherEvent;
const eventsPerDay=6; // how much different events per day we can display
const date = new Date();
const timeutils = require("time_utils");
let settings = require('Storage').readJSON("calendar.json", true) || {};
let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0;
let events;
const dowLbls = function() {
const locale = require('locale').name;
const days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0];
const d = new Date();
return days.map(i => {
d.setDate(d.getDate() + (i + 7 - d.getDay()) % 7);
return require("locale").dow(d, 1);
});
}();
const loadEvents = () => {
// all alarms that run on a specific date
const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
const date = new Date(a.date);
const time = timeutils.decodeTime(a.t);
date.setHours(time.h);
@ -49,11 +61,13 @@ const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a
}
events.push(o);
});
};
const loadSettings = () => {
let settings = require('Storage').readJSON("calendar.json", true) || {};
if (settings.ndColors === undefined) {
settings.ndColors = !g.theme.dark;
}
if (settings.ndColors === true) {
bgColor = white;
bgColorMonth = blue;
@ -63,25 +77,26 @@ if (settings.ndColors === true) {
fgSameMonth = black;
bgEvent = color2;
bgOtherEvent = cyan;
} else {
bgColor = color4;
bgColorMonth = color1;
bgColorDow = color2;
bgColorWeekend = color3;
fgOtherMonth = gray1;
fgSameMonth = white;
bgEvent = blue;
bgOtherEvent = "#ff8800";
}
};
function getDowLbls(locale) {
let days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0];
const d = new Date();
return days.map(i => {
d.setDate(d.getDate() + (i + 7 - d.getDay()) % 7);
return require("locale").dow(d, 1);
});
}
function sameDay(d1, d2) {
const sameDay = function(d1, d2) {
"jit";
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
};
function drawEvent(ev, curDay, x1, y1, x2, y2) {
const drawEvent = function(ev, curDay, x1, y1, x2, y2) {
"ram";
switch(ev.type) {
case "e": // alarm/event
@ -99,9 +114,33 @@ function drawEvent(ev, curDay, x1, y1, x2, y2) {
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
break;
}
}
};
function drawCalendar(date) {
const calcDays = (month, monthMaxDayMap, dowNorm) => {
"jit";
const maxDay = colN * (rowN - 1) + 1;
const days = [];
let nextMonthDay = 1;
let thisMonthDay = 51;
const month2 = month;
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1;
for (let i = 0; i < maxDay; i++) {
if (i < dowNorm) {
days.push(prevMonthDay);
prevMonthDay++;
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
days.push(thisMonthDay);
thisMonthDay++;
} else {
days.push(nextMonthDay);
nextMonthDay++;
}
}
return days;
};
const drawCalendar = function(date) {
g.setBgColor(bgColor);
g.clearRect(0, 0, maxX, maxY);
g.setBgColor(bgColorMonth);
@ -139,7 +178,6 @@ function drawCalendar(date) {
true
);
let dowLbls = getDowLbls(require('locale').name);
dowLbls.forEach((lbl, i) => {
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
});
@ -163,23 +201,7 @@ function drawCalendar(date) {
11: 31
};
let days = [];
let nextMonthDay = 1;
let thisMonthDay = 51;
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1;
for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
if (i < dowNorm) {
days.push(prevMonthDay);
prevMonthDay++;
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
days.push(thisMonthDay);
thisMonthDay++;
} else {
days.push(nextMonthDay);
nextMonthDay++;
}
}
const days = calcDays(month, monthMaxDayMap, dowNorm);
const weekBeforeMonth = new Date(date.getTime());
weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7);
const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
@ -189,8 +211,15 @@ function drawCalendar(date) {
ev.date.setFullYear(ev.date.getMonth() < 6 ? week2AfterMonth.getFullYear() : weekBeforeMonth.getFullYear());
}
});
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
eventsThisMonth.sort((a,b) => a.date - b.date);
const eventsThisMonthPerDay = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth).reduce((acc, ev) => {
const day = ev.date.getDate();
if (!acc[day]) {
acc[day] = [];
}
acc[day].push(ev);
return acc;
}, []);
let i = 0;
g.setFont("8x12", fontSize);
for (y = 0; y < rowN - 1; y++) {
@ -205,13 +234,13 @@ function drawCalendar(date) {
const x2 = x * colW + colW;
const y2 = y * rowH + headerH + rowH + rowH;
if (eventsThisMonth.length > 0) {
const eventsThisDay = eventsThisMonthPerDay[curDay.getDate()];
if (eventsThisDay && eventsThisDay.length > 0) {
// Display events for this day
eventsThisMonth.forEach((ev, idx) => {
eventsThisDay.forEach((ev, idx) => {
if (sameDay(ev.date, curDay)) {
drawEvent(ev, curDay, x1, y1, x2, y2);
eventsThisMonth.splice(idx, 1); // this event is no longer needed
eventsThisDay.splice(idx, 1); // this event is no longer needed
}
});
}
@ -235,9 +264,44 @@ function drawCalendar(date) {
);
} // end for (x = 0; x < colN; x++)
} // end for (y = 0; y < rowN - 1; y++)
} // end function drawCalendar
}; // end function drawCalendar
const showMenu = function() {
const menu = {
"" : {
title : "Calendar",
remove: () => {
require("widget_utils").show();
}
},
"< Back": () => {
require("widget_utils").hide();
E.showMenu();
setUI();
},
/*LANG*/"Exit": () => load(),
/*LANG*/"Settings": () => {
const appSettings = eval(require('Storage').read('calendar.settings.js'));
appSettings(() => {
loadSettings();
loadEvents();
showMenu();
});
},
};
if (require("Storage").read("alarm.app.js")) {
menu[/*LANG*/"Launch Alarms"] = () => {
load("alarm.app.js");
};
}
require("widget_utils").show();
E.showMenu(menu);
};
const setUI = function() {
require("widget_utils").hide(); // No space for widgets!
drawCalendar(date);
function setUI() {
Bangle.setUI({
mode : "custom",
swipe: (dirLR, dirUD) => {
@ -261,7 +325,14 @@ function setUI() {
drawCalendar(date);
}
},
btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && load(),
btn: (n) => {
if (process.env.HWVERSION === 2 || n === 2) {
showMenu();
} else if (n === 3) {
// directly exit only on Bangle.js 1
load();
}
},
touch: (n,e) => {
events.sort((a,b) => a.date - b.date);
const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => {
@ -274,16 +345,19 @@ function setUI() {
}
menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() };
menu["< Back"] = () => {
require("widget_utils").hide();
E.showMenu();
drawCalendar(date);
setUI();
};
require("widget_utils").show();
E.showMenu(menu);
}
});
}
};
loadSettings();
loadEvents();
Bangle.loadWidgets();
require("Font8x12").add(Graphics);
drawCalendar(date);
setUI();
// No space for widgets!
}

View File

@ -178,9 +178,8 @@ function getData() {
})()\n`, contents => {
const fileNames = JSON.parse(contents);
if (fileNames.length > 0) {
Util.readStorage('calendar.days.json',data=>{
holidays = JSON.parse(data || "[]") || [];
Util.readStorageJSON('calendar.days.json',data=>{
holidays = data || [];
Util.hideModal();
render();
});

View File

@ -1,8 +1,8 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.15",
"description": "Simple calendar",
"version": "0.16",
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
"tags": "calendar,tool",

View File

@ -1,14 +1,15 @@
(function (back) {
var FILE = "calendar.json";
const FILE = "calendar.json";
const HOLIDAY_FILE = "calendar.days.json";
var settings = require('Storage').readJSON(FILE, true) || {};
if (settings.ndColors === undefined)
const settings = require('Storage').readJSON(FILE, true) || {};
if (settings.ndColors === undefined) {
if (process.env.HWVERSION == 2) {
settings.ndColors = true;
} else {
settings.ndColors = false;
}
const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
}
const holidays = (require("Storage").readJSON(HOLIDAY_FILE,1)||[]).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
function writeSettings() {
require('Storage').writeJSON(FILE, settings);

View File

@ -1 +1,2 @@
0.01: Simple app to display loyalty cards
0.02: Hiding widgets while showing the code

View File

@ -19,33 +19,11 @@ This app is a proof of concept, many codes are too complex to be rendered by the
### How to sync
_WIP: we currently cannot synchronize cards, a PR is under review in GadgetBridge repo, soon we will see support on nightly builds_
We can synchronize cards with GadgetBridge and Catima, refer to those projects for further information.
The feature is currently available on nightly builds only.
It should be released from version 0.77 (not yet out at the time of writing).
You can test it by sending on your bangle a file like this:
_android.cards.json_
```json
[
{
"id": 1,
"name": "First card",
"value": "01234",
"note": "Some stuff",
"type": "CODE_39",
"balance": "15 EUR",
"expiration": "1691102081"
},
{
"id": 2,
"name": "Second card",
"value": "Hello world",
"note": "This is a qr generated on the bangle!",
"type": "QR_CODE",
"balance": "2 P"
}
]
```
GadgetBridge syncronizes all cards at once, if you have too many cards you may want to explicitly select which ones to syncronize, keep in mind the limitations of the Banglejs.
### Credits

View File

@ -18,9 +18,8 @@ Bangle.drawWidgets();
const WHITE=-1
const BLACK=0
var FILE = "android.cards.json";
var Locale = require("locale");
const Locale = require("locale");
const widget_utils = require('widget_utils');
var fontSmall = "6x8";
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
@ -90,6 +89,7 @@ function printLinearCode(binary) {
}
function showCode(card) {
widget_utils.hide();
E.showScroller();
// keeping it on rising edge would come back twice..
setWatch(()=>showCard(card), BTN, {edge:"falling"});
@ -151,6 +151,7 @@ function showCard(card) {
var titleColor = g.theme.fg2;
if (card.color)
titleColor = isLight(titleBgColor) ? BLACK : WHITE;
widget_utils.show();
E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
c : lines.length, // number of menu items

View File

@ -1,7 +1,7 @@
{
"id": "cards",
"name": "Cards",
"version": "0.01",
"version": "0.02",
"description": "Display loyalty cards",
"icon": "app.png",
"screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}],

View File

@ -1 +1,2 @@
0.01: New App!
0.02: added settings options to change date format

View File

@ -1,5 +1,20 @@
(function() {
require("Font4x8Numeric").add(Graphics);
var settings = require("Storage").readJSON("clkinfocal.json",1)||{};
settings.fmt = settings.fmt||"DDD";
var getDateString = function(dt) {
switch(settings.fmt) {
case "dd MMM":
return '' + dt.getDate() + ' ' + require("locale").month(dt,1).toUpperCase();
case "DDD dd":
return require("locale").dow(dt,1).toUpperCase() + ' ' + dt.getDate();
default: // DDD
return require("locale").dow(dt,1).toUpperCase();
}
};
return {
name: "Bangle",
items: [
@ -10,7 +25,7 @@
g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0);
g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17);
return {
text : require("locale").dow(d,1).toUpperCase(),
text : getDateString(d),
img : g.asImage("string")
};
},

View File

@ -1,13 +1,15 @@
{ "id": "clkinfocal",
"name": "Calendar Clockinfo",
"version":"0.01",
"description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday",
"version":"0.02",
"description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday. There is also a settings menu to select the format of the text",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo,calendar",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"}
]
{"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"},
{"name":"clkinfocal.settings.js","url":"settings.js"}
],
"data": [{"name":"clkinfocal.json"}]
}

View File

@ -0,0 +1,37 @@
(function(back) {
const SETTINGS_FILE = "clkinfocal.json";
// initialize with default settings...
let s = {'fmt': 0};
// and overwrite them with any saved values
// this way saved values are preserved if a new version adds more settings
const storage = require('Storage');
let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
const saved = settings || {};
for (const key in saved) {
s[key] = saved[key];
}
function save() {
settings = s;
storage.write(SETTINGS_FILE, settings);
}
var date_options = ["DDD","DDD dd","dd MMM"];
E.showMenu({
'': { 'title': 'Cal Clkinfo' },
'< Back': back,
'Format': {
value: 0 | date_options.indexOf(s.fmt),
min: 0, max: 2,
format: v => date_options[v],
onchange: v => {
s.fmt = date_options[v];
save();
},
}
});
});

View File

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

BIN
apps/clkinfoclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

View File

@ -0,0 +1,27 @@
(function() {
return {
name: "Bangle",
items: [
{ name : "Clock",
get : () => {
return {
text : require("locale").time(new Date(),1),
img : atob("FhaBAAAAAAPwAD/wA8DwHADgYMGDAwMMDAxgMBmAwGYDAZgOBmAcGYA4YwBjDAAMGABgcAOA8DwA/8AA/AAAAAA=")
};
},
show : function() {
this.interval = setTimeout(()=>{
this.emit("redraw");
this.interval = setInterval(()=>{
this.emit("redraw");
}, 60000);
}, 60000 - (Date.now() % 60000));
},
hide : function() {
clearInterval(this.interval);
this.interval = undefined;
}
}
]
};
})

View File

@ -0,0 +1,13 @@
{ "id": "clkinfoclk",
"name": "Clockinfo Clock",
"version":"0.01",
"description": "This displays a clock *inside* a ClockInfo. This can be really handy for the [Clock Info Widget](https://banglejs.com/apps/?id=widclkinfo) where you might want the option to show a clock in the top bar of a non-clock app.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfoclk.clkinfo.js","url":"clkinfo.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,14 @@
# Clock Switcher
This switches the default clock.
The idea is that you can use this app in combination with e.g. the
[Pattern Launcher](?q=ptlaunch) as a quick toggle, instead of navigating through
the settings menu.
## Usage
Load the app to switch to your next installed clock.
## Creator
Richard de Boer (rigrig)

23
apps/clockswitch/app.js Normal file
View File

@ -0,0 +1,23 @@
const storage = require('Storage');
const clocks = storage.list(/\.info$/)
.map(app => {
const a=storage.readJSON(app, 1);
return (a && a.type == "clock") ? a : undefined;
})
.filter(app => app) // filter out any undefined apps
.sort((a, b) => a.sortorder - b.sortorder)
.map(app => app.src);
if (clocks.length<1) {
E.showAlert(/*LANG*/"No clocks found!", "Clock Switcher")
.then(load);
} else if (clocks.length<2) {
E.showAlert(/*LANG*/"Nothing to do:\nOnly one clock installed!", "Clock Switcher")
.then(load);
} else {
let settings = storage.readJSON('setting.json',true)||{clock:null};
const old = clocks.indexOf(settings.clock),
next = (old+1)%clocks.length;
settings.clock = clocks[next];
storage.writeJSON('setting.json', settings);
setTimeout(load, 100); // storage.writeJSON needs some time to complete
}

1
apps/clockswitch/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AClhCyoAYsIwuF4IwtF4Qxqw2GF4mG1YsqAAeF1eyAAIteFhAvHGLeGwouLR4IuEGDJcGwooBAweH6/X6wwGGKtbKownB640C1gGCAAQwZLgotDF4WG6wuFMZAuVw2yEgqLCABIuD1eGF6eGExYwLw4bCF1BuCDgWFdaGFRgwAJlb0HJogvPdQoAKq0AlYJG1YwDRr+sgEAL4wABwxgNF4ZeSqwLIMAYvNwpebAAOFSBgMCw7sQLxSQORwZLKLw4OLSBlbBgWyLznX2RfPLqBeM6/WcQYvZldbrYvN64jDF7rRNF7qPDGBqPLd6YxDGBTvQPpowQ1YvLGAeHF54wDlYMIwwvPwovQGAIuJ6+FdxSQF1YwRABKONF4mGF7aONAANbMDpeDRxRgFsOyFy+yP4gvLMAiRX6yNDwouMGDYuELxyRGwySS2QuUMAr0SdQguSGA+G1gtMLgguUGAQxFwuH1aWE2QsBwoQEFyzEHAB+FFzAwCMQoALFrRiRwwtefI5mCQwIslAH4A/AFw"))

BIN
apps/clockswitch/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,14 @@
{ "id": "clockswitch",
"name": "Clock Switcher",
"shortName":"Switch Clock",
"version":"0.01",
"description": "Switch to the next installed clock",
"icon": "icon.png",
"tags": "tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"clockswitch.app.js","url":"app.js"},
{"name":"clockswitch.img","url":"icon.js","evaluate":true}
]
}

1
apps/contacts/ChangeLog Normal file
View File

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

29
apps/contacts/README.md Normal file
View File

@ -0,0 +1,29 @@
# Contacts
This app provides a common way to set up the `contacts.json` file.
## Contacts JSON file
When the app is loaded from the app loader, a file named
`contacts.json` is loaded along with the javascript etc. The file
has the following contents:
```
[
{
"name":"NONE"
},
{
"name":"First Last",
"number":"123456789",
}
]
```
## Contacts Editor
Clicking on the download icon of `Contents` in the app loader invokes
the contact editor. The editor downloads and displays the current
`contacts.json` file. Clicking the `Edit` button beside an entry
causes the entry to be deleted from the list and displayed in the edit
boxes. It can be restored - by clicking the `Add` button.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcBkmSpIC/AVsJCJ+AQaCZBCOeACKGQLKGQBA0ggARPJ4IRsYo0ggR9IoAIGiRiIpEECJsAiACBBYoRGpEAI4JBFI47CBLIRlDHYJrGYQIRCwQICL4MQOgx9GboUSeQ4RFwAFBiSGHCIo4CiVIWZyPICP4RaRIQROgARHdIwICoIIFkDpGBAKqHgGACI0AyVIggIDoEEMQ1ICINJCIj4CfwIREBwUgQYYOCfYoFDJQKDFCIopEO4RoDKAqJHRhAC/ATA="))

BIN
apps/contacts/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,189 @@
/* contacts.js */
var Layout = require("Layout");
const W = g.getWidth();
const H = g.getHeight();
var wp = require('Storage').readJSON("contacts.json", true) || [];
// Use this with corrupted contacts
//var wp = [];
var key; /* Shared between functions, typically wp name */
function writeContact() {
require('Storage').writeJSON("contacts.json", wp);
}
function mainMenu() {
var menu = {
"< Back" : Bangle.load
};
if (Object.keys(wp).length==0) Object.assign(menu, {"NO Contacts":""});
else for (let id in wp) {
let i = id;
menu[wp[id]["name"]]=()=>{ decode(i); };
}
menu["Add"]=addCard;
menu["Remove"]=removeCard;
g.clear();
E.showMenu(menu);
}
function decode(pin) {
var i = wp[pin];
var l = i["name"] + "\n" + i["number"];
var la = new Layout ({
type:"v", c: [
{type:"txt", font:"10%", pad:1, fillx:1, filly:1, label: l},
{type:"btn", font:"10%", pad:1, fillx:1, filly:1, label:"OK", cb:l=>{mainMenu();}}
], lazy:true});
g.clear();
la.render();
}
function showNumpad(text, key_, callback) {
key = key_;
E.showMenu();
function addDigit(digit) {
key+=digit;
if (1) {
l = text[key.length];
switch (l) {
case '.': case ' ': case "'":
key+=l;
break;
case 'd': case 'D': default:
break;
}
}
Bangle.buzz(20);
update();
}
function update() {
g.reset();
g.clearRect(0,0,g.getWidth(),23);
s = key + text.substr(key.length, 999);
g.setFont("Vector:24").setFontAlign(1,0).drawString(s,g.getWidth(),12);
}
ds="12%";
var numPad = new Layout ({
type:"v", c: [{
type:"v", c: [
{type:"", height:24},
{type:"h",filly:1, c: [
{type:"btn", font:ds, width:58, label:"7", cb:l=>{addDigit("7");}},
{type:"btn", font:ds, width:58, label:"8", cb:l=>{addDigit("8");}},
{type:"btn", font:ds, width:58, label:"9", cb:l=>{addDigit("9");}}
]},
{type:"h",filly:1, c: [
{type:"btn", font:ds, width:58, label:"4", cb:l=>{addDigit("4");}},
{type:"btn", font:ds, width:58, label:"5", cb:l=>{addDigit("5");}},
{type:"btn", font:ds, width:58, label:"6", cb:l=>{addDigit("6");}}
]},
{type:"h",filly:1, c: [
{type:"btn", font:ds, width:58, label:"1", cb:l=>{addDigit("1");}},
{type:"btn", font:ds, width:58, label:"2", cb:l=>{addDigit("2");}},
{type:"btn", font:ds, width:58, label:"3", cb:l=>{addDigit("3");}}
]},
{type:"h",filly:1, c: [
{type:"btn", font:ds, width:58, label:"0", cb:l=>{addDigit("0");}},
{type:"btn", font:ds, width:58, label:"C", cb:l=>{key=key.slice(0,-1); update();}},
{type:"btn", font:ds, width:58, id:"OK", label:"OK", cb:callback}
]}
]}
], lazy:true});
g.clear();
numPad.render();
update();
}
function removeCard() {
var menu = {
"" : {title : "Select Contact"},
"< Back" : mainMenu
};
if (Object.keys(wp).length==0) Object.assign(menu, {"No Contacts":""});
else {
wp.forEach((val, card) => {
const name = wp[card].name;
menu[name]=()=>{
E.showMenu();
var confirmRemove = new Layout (
{type:"v", c: [
{type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:"Delete"},
{type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:name},
{type:"h", c: [
{type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{
wp.splice(card, 1);
writeContact();
mainMenu();
}},
{type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}}
]}
], lazy:true});
g.clear();
confirmRemove.render();
};
});
}
E.showMenu(menu);
}
function askPosition(callback) {
let full = "";
showNumpad("dddDDDddd", "", function() {
callback(key, "");
});
}
function createContact(lat, name) {
let n = {};
n["name"] = name;
n["number"] = lat;
wp.push(n);
print("add -- contacts", wp);
writeContact();
}
function addCardName2(key) {
g.clear();
askPosition(function(lat, lon) {
print("position -- ", lat, lon);
createContact(lat, result);
mainMenu();
});
}
function addCardName(key) {
result = key;
if (wp[result]!=undefined) {
E.showMenu();
var alreadyExists = new Layout (
{type:"v", c: [
{type:"txt", font:Math.min(15,100/result.length)+"%", pad:1, fillx:1, filly:1, label:result},
{type:"txt", font:"12%", pad:1, fillx:1, filly:1, label:"already exists."},
{type:"h", c: [
{type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "REPLACE", cb:l=>{ addCardName2(key); }},
{type:"btn", font:"10%", pad:1, fillx:1, filly:1, label: "CANCEL", cb:l=>{mainMenu();}}
]}
], lazy:true});
g.clear();
alreadyExists.render();
return;
}
addCardName2(key);
}
function addCard() {
require("textinput").input({text:""}).then(result => {
if (result != "") {
addCardName(result);
} else
mainMenu();
});
}
g.reset();
Bangle.setUI();
mainMenu();

View File

@ -0,0 +1,6 @@
[
{
"name":"EU emergency",
"number":"112"
}
]

View File

@ -0,0 +1,286 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css">
<script type="module">
import vcf from 'https://cdn.jsdelivr.net/npm/vcf@2.1.1/+esm'
window.vcf = vcf;
</script>
<style type="text/css">
html, body { height: 100% }
.flex-col { display:flex; flex-direction:column; height:100% }
#map { width:100%; height:100% }
#tab-list { width:100%; height:100% }
/* https://stackoverflow.com/a/58686215 */
.arrow-icon {
width: 14px;
height: 14px;
}
.arrow-icon > div {
margin-left: -1px;
margin-top: -3px;
transform-origin: center center;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
</style>
</head>
<body>
<h1>Contacts v.2</h1>
<div class="flex-col">
<div id="statusarea">
<button id="download" class="btn btn-error">Reload</button> <button id="upload" class="btn btn-primary">Upload</button>
<span id="status"></span>
<span id="routestatus"></span>
</div>
<div style="flex: 1">
<div id="tab-list">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Number</th>
<th></th>
</tr>
</thead>
<tbody id="contacts">
</tbody>
</table>
<br>
<h4>Add a new contact</h4>
<form id="add_contact_form">
<div class="columns">
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" type="text" id="add_contact_name" placeholder="Name">
</div>
<div class="column col-3 col-xs-8">
<input class="form-input input-sm" value="123456789" type="text" id="add_number" placeholder="Number">
</div>
</div>
<div class="columns">
<div class="column col-3 col-xs-8">
<button id="add_contact_button" class="btn btn-primary btn-sm">Add Contact</button>
</div>
</div>
</form>
<div class="divider"></div>
<div class="form-horizontal">
<div class="form-group">
<div class="col-5 col-xs-12">
<label class="form-label" for="fileinput">Add from vCard file</label>
</div>
<div class="col-7 col-xs-12">
<input id="fileinput" class="form-input" type="file" onchange="readFile(this)" accept=".vcf" multiple/>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="../../core/lib/interface.js"></script>
<script>
var contacts = [];
// ==========================================================================
/*** status ***/
function clean() {
$('#status').html('<i class="icon icon-check"></i> No pending changes.');
}
function dirty() {
$('#status').html('<b><i class="icon icon-edit"></i> Changes have not been sent to the watch.</b>');
}
/*** contacts ***/
function deleteContact(arr, i) {
arr.splice(i, 1);
renderAllContacts();
dirty();
}
function renameContact(arr, i) {
var name = prompt("Enter new name for the contact:", arr[i].name);
if (name == null || name == "" || name == arr[i].name)
return;
arr[i].name = name;
renderAllContacts();
dirty();
}
/*** util ***/
// https://stackoverflow.com/a/22706073
function escapeHTML(str){
return new Option(str).innerHTML;
}
/*** Bangle.js ***/
function gotStored(pts) {
contacts = pts;
renderAllContacts();
}
// ========================================================================== LIST
var $name = document.getElementById('add_contact_name')
var $form = document.getElementById('add_contact_form')
var $button = document.getElementById('add_contact_button')
var $number = document.getElementById('add_number')
var $list = document.getElementById('contacts')
function compare(a, b){
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
if (x=="none") {return -1};
if (y=="none") {return 1};
if (x < y) {return -1;}
if (x > y) {return 1;}
return 0;
}
$button.addEventListener('click', event => {
event.preventDefault()
var name = $name.value.trim()
if(!name) return;
var number = $number.value.trim();
contacts.push({
name, number,
});
contacts.sort(compare);
renderAllContacts()
$name.value = ''
$number.value = (0);
dirty();
});
function removeContact(index){
$name.value = contacts[index].name
$number.value = contacts[index].number
contacts = contacts.filter((p,i) => i!==index)
renderAllContacts()
}
function renderContactsList(){
$list.innerHTML = ''
contacts.forEach((contact,index) => {
var $contact = document.createElement('tr')
if(contact.number==undefined){
$contact.innerHTML = `<td>${contact.name}</td><td>(no number)</td>`;
} else {
$contact.innerHTML = `<td>${contact.name}</td><td><a href="tel:${contact.number}">${contact.number}</a></td>`;
}
let buttons = `<button class="btn btn-action btn-error" onclick="removeContact(${index})"><i class="icon icon-delete"></i></button>`;
buttons += `<button class="btn btn-action" onclick="exportVcard(${index})" title="Export vCard"><i class="icon icon-download"></i></button>`;
$contact.innerHTML += `<td>${buttons}</td>`;
$list.appendChild($contact)
})
$name.focus()
}
function renderContacts() {
renderContactsList();
}
function renderAllContacts() {
renderContactsList();
}
function readFile(input) {
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
let vcards;
try {
vcards = vcf.parse(reader.result);
} catch (error) {
alert(error);
return;
}
vcards.forEach(vcard => {
const name = vcard.get('fn')?.valueOf() || vcard.get('n')?.valueOf();
const tels = Array.isArray(vcard.get('tel')) ? vcard.get('tel') : [vcard.get('tel')];
tels.forEach(tel => {
if (tel) {
const number = tel.valueOf();
contacts.push({name: name, number: number});
}
});
});
renderAllContacts();
dirty();
}, false);
reader.readAsText(input.files[i], "UTF-8");
}
}
function exportVcard(index){
const vCard = new vcf();
vCard.set('n', contacts[index].name);
vCard.set('tel', contacts[index].number);
Util.saveFile(contacts[index].name+".vcf", "text/vcard", vCard.toString());
}
// ========================================================================== UPLOAD/DOWNLOAD
function downloadJSONfile(fileid, callback) {
// TODO: use interface.js-provided stuff?
Puck.write(`\x10(function() {
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
Bluetooth.print(JSON.stringify(pts));
})()\n`, contents => {
if (contents=='[{name:"NONE"}]') contents="[]";
var storedpts = JSON.parse(contents);
callback(storedpts);
clean();
});
}
function uploadFile(fileid, contents) {
// TODO: use interface.js-provided stuff?
Puck.write(`\x10(function() {
require("Storage").write("${fileid}",'${contents}');
Bluetooth.print("OK");
})()\n`, ret => {
console.log("uploadFile", ret);
if (ret == "OK")
clean();
});
}
function onInit() {
downloadJSONfile("contacts.json", gotStored);
}
$('#download').on('click', function() {
downloadJSONfile("contacts.json", gotStored);
});
$('#upload').click(function() {
var data = JSON.stringify(contacts);
uploadFile("contacts.json",data);
});
// ========================================================================== FINALLY...
clean();
renderAllContacts();
</script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{ "id": "contacts",
"name": "Contacts",
"version":"0.01",
"description": "Provides means of storing user contacts, viewing/editing them on device and from the App loader",
"icon": "app.png",
"tags": "tool",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"interface": "interface.html",
"dependencies": {"textinput":"type"},
"storage": [
{"name":"contacts.app.js","url":"contacts.app.js"},
{"name":"contacts.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"contacts.json","url":"contacts.json"}
]
}

1
apps/dwm-clock/ChangeLog Normal file
View File

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

7
apps/dwm-clock/README.md Normal file
View File

@ -0,0 +1,7 @@
A clock with a daylight world map
The function for the daylight graph is a crude approximation for an equirectangular projection of a circle on a sphere.
You can change the longitudinal map offset by swiping the map sideways. For saving the changes to the file dwm-clock.json press the top left quarter of the screen. To discard changes press the top right quarter.
If you are interested in changing the vector font to another one, please do.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4n/AoPg/8fAYM5sGt4FakFjsFKgHGv9rsEpoHOkGl0ExsHvj0AkQAugHyAYMv/4ECkX/BwfwAogACgQDCl8PBQnwBQZ3BC4opDAAg0BGIP/+CVCAoPykAiBiIABjOZBwIMB/8PUYQAJ4AYCiuZzLLQCwUYgEEgGZuAXQ5bEBqMAgeTChaHCiMc2cRmFTBQMJ0AXMl8Rj0QqALEyAwL+QXBGAIKFhJHOjcMglz4EMjew4IwJWILVBiMWPQNOstABgMBsxFJkAvCiNsmc+AgPqSIJIIe4KPEBAMG9FstQFCJA/y/4WBC4PNBo0IhQXICwUijy3FglggtKjIXMBYsKwAaBjPwRIICBIgYXJgGFgHBiMvZ4QtCgQXLhXOSgOfQ4TTCAoR3CuAYGhgXBySiBCgIvB+AXEisA8AXEgIXBiUvkUvLwP/AoIXDAATtCAAPhC4QOBL4Mvh//SAYXHhwHCjIPC+Hwh8CkCTCC4cWFwoXEJAIUCPgPyC4/RBAeSLIZiCAYIvIgO0C45LCI5WLR4RIEAAMgIwLaBC4cbCwMIigXI/4lCbQIXDiLVCjdhMA7NEO4kRmEACogXF+AXDgQOEiEGCwrBFC5MeXoYXHl7uBDQJHFABKoFYQQXOYIzXFABUSC4rXFABTZFC6JIHC6EZDAwXPJI4PIilBAgdEAAOZz//UwIXCj3r3YABxGwcgVLwDqDgF5zOQgEPj3QBYgANhOZzMwCyQA/ABYA="))

224
apps/dwm-clock/app.js Normal file
View File

@ -0,0 +1,224 @@
// daylight world map clock
// equirectangular projected map and approximated daylight graph
// load font for timezone, weekday and day in month
require("FontDennis8").add(Graphics);
const W = g.getWidth();
const H = g.getHeight();
const TZOFFSET = new Date().getTimezoneOffset();
const UTCSTRING = ((TZOFFSET > 0 ? "-" : "+")
+ ("0" + Math.floor(Math.abs(TZOFFSET) / 60)).slice(-2))
+ (TZOFFSET % 60 ? Math.abs(TZOFFSET) % 60 : "");
function getMap() {
return {
width: 176, height: 88, bpp: 1,
transparent: 1,
buffer: require("heatshrink").decompress(atob("/4A/AA0Av+Ag4UQwBhDn//1//8///AUI3MAhAUBgIQBh4LC/kfCg34rmAngVD/1/CYICBA4IAF8EOwF/+AVCAAXj//AA4PjDQIVDgkQj/4gBtEx+EGgXwCoJ8Bv+8geQgIVE4P/553Egf/nwFCgUE4H8gBqB/0AhLxHggFE+E8gJoBDIIAI5wFE4F8h/4v5FBABA2BAAUf7n+VYXgoAVNn/Dv+fCoPACo8MEQPHHAUf4DuB//58FgCgsHeoWfMgUDConw4AVFh/wXIRDDwBWC8jfBFY3xaAa5DYYXkKw8D+YVDHAcXAwKuIgIUDSIIJCsYVKeAIVHj5fGNogVHgN/AwPyEgPhCokZCo40D8E0wcwTYhsECoY0D8H2hEACocBCoqnCKwQVB/nICokJ+4VL/RGBQQkdw4VESQTwCDgIVBNgkeEQaSEQQReC4QrEhwUECoUECooAFVwoABgF+CoY+DAYZAFAAOgv4VGoFgCpXwGIoABkEHDQUvCo9zD4YVE4EIgIUGCoNnZwYVCiEP8E8hYVH/kHII0Qj/wvkP94WH4IVGhE/MQMH54VH+IVGKYIJBgfnCo/98IVFcYP5/9HMYbdGn7FFv/4/9vCpH/4DmC4AVCD4P/n4VKUoXgCwQ2Cz42CCpX//BtCCoMeCpJTBZgcAgYFCjElCpA7BEIQVBZoeYp4sICoIQCIIJzC/+Mp+DCpJSC/kAj4KC5/f4GfK5AVIeYPgNpIVEIIf/6f/v6ZHPwYVG//7V5BtDCoMOEof+jYVH8AVFhgLD/EZCo6UBCokYBYa2BCp04G4oVJNAX+gF4XYqDHCoKqCCoIrDAoL9DCowfCB4N9CorMDCooPEfowVMB4IVPeAQABwIVPeAQABw4LEg/ANo/wTAQAI8E//YVS+F//IIGGg4AFCo7OHAAf+v/jCowqM//HAwvhCpuPOwwVNAAwrOAA3xCqhtOAH4AfW4wAN/0/A4sP//AgFygYVH/V/AwlwgE8gAACDYIAF9ArC+uACAUgCocAHIn8k/gj4FBCgYAGBoXwgEYDof+ChMAJ4PmAwcBDgIUKgANBJIkZ/0cCpYrBIAIADzkwChQ5B/tgBAh7FNpANMAGg="))
};
}
const YOFFSET = H - getMap().height;
// map offset in degree
// -180 to 180 / default: 0
function getLongitudeOffset() {
return require("Storage").readJSON("dwm-clock.json", 1) || {"lon": 0};
}
function drawMap() {
g.setBgColor(0, 0, 0);
// does not flip on it's own, but there is a draw function after that does
g.drawImages([{
x: -lonOffset * W / 360,
y: YOFFSET,
image: getMap(),
scale: 1,
rotate: 0,
center: false,
repeat: true,
nobounds: false
}], {
x: 0,
y: YOFFSET,
width: getMap().width,
height: getMap().height
});
}
function drawDaylightMap() {
// number of xy points, < 40 looks very skewed around solstice
const STEPS = 40;
const YFACTOR = getMap().height / 2;
const YOFF = H / 2 + YFACTOR;
var graph = [];
// progress of day, float 0 to 1
var dayOffset = (now.getHours() + (now.getMinutes() + TZOFFSET) / 60) / 24;
// sun position modifier
var sunPosMod;
var solarNoon = require("suncalc").getTimes(now, 0, 0, 0).solarNoon;
var altitude = require("suncalc").getPosition(solarNoon, 0, 0).altitude;
// this is trial and error. no thought went into this
sunPosMod = Math.pow(altitude - 0.08, 8);
// switch sign on equinox
// this is an approximation
if (require("suncalc").getPosition(solarNoon, 0, 0).azimuth < -1) {
sunPosMod = -sunPosMod;
}
for (var x = 0; x < (STEPS + 1) / STEPS; x += 1 / STEPS) {
// this is an approximation instead of projecting a circle onto a sphere
// y = arctan(sin(x) * n)
var y = Math.atan(Math.sin(2 * Math.PI * x + dayOffset * 2 * Math.PI
// user defined map offset fixed offset
// v v
+ 2 * Math.PI * lonOffset / 360 - Math.PI / 2) * sunPosMod)
* (2 / Math.PI);
// ^
// factor keeps y <= 1
graph.push(x * W, y * YFACTOR + YOFF);
}
// day area, yellow
g.setColor(0.8, 0.8, 0.3);
g.fillRect(0, YOFFSET, W, H);
// night area, blue
g.setColor(0, 0, 0.5);
// switch on equinox
if (sunPosMod < 0) {
g.fillPoly([0, H - 1].concat(graph, W - 1, H - 1));
} else {
g.fillPoly([0, YOFFSET].concat(graph, W, YOFFSET));
}
drawMap();
// day-night line, white
g.setColor(1, 1, 1);
g.drawPoly(graph, false);
}
function drawClock() {
// clock area
g.clearRect(0, YOFFSET, W, 24);
// clock text
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.setFont("Vector", 58);
// with the vector font this leaves 26px above the text
g.drawString(require("locale").time(now, 1), W / 2, 24 - 2);
// timezone text
g.setFontAlign(-1, 1);
g.setFont("6x8", 2);
g.drawString("UTC" + UTCSTRING, 3, YOFFSET);
// day text
g.setFontAlign(1, 1);
g.setFont("Dennis8", 2);
g.drawString(require("locale").dow(now, 1) + " " + now.getDate(),
W - 1, YOFFSET);
}
function renderScreen() {
now = new Date();
drawClock();
drawDaylightMap();
}
function renderAndQueue() {
timeoutID = setTimeout(renderAndQueue, 60000 - (Date.now() % 60000));
renderScreen();
}
g.reset().clearRect(Bangle.appRect);
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
g.setBgColor(0, 0, 0);
var now = new Date();
// map offsets
var defLonOffset = getLongitudeOffset().lon;
var lonOffset = defLonOffset;
var timeoutID;
var timeoutIDTouch;
Bangle.on('drag', function(touch) {
if (timeoutIDTouch) {
clearTimeout(timeoutIDTouch);
}
// return after not touching for 5 seconds
timeoutIDTouch = setTimeout(renderAndQueue, 5 * 1000);
// touch map
if (touch.y >= YOFFSET) {
lonOffset -= touch.dx * 360 / W;
// wrap map offset
if (lonOffset < -180) {
lonOffset += 360;
} else if (lonOffset >= 180) {
lonOffset -= 360;
}
// snap to 0° longitude
if (lonOffset > -5 && lonOffset < 5) {
lonOffset = 0;
}
lonOffset = Math.round(lonOffset);
// clock area
g.clearRect(0, YOFFSET, W, 24);
// text
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.setFont("Dennis8", 2);
// could not get ° (degree sign) to render
g.drawString("select lon offset\n< tap: save\nreset: tap >\n"
+ lonOffset + " degree", W / 2, 24);
drawDaylightMap();
// touch clock, left side, save offset
} else if (touch.x < W / 2) {
if (defLonOffset != lonOffset) {
require("Storage").writeJSON("dwm-clock.json", {"lon": lonOffset});
defLonOffset = lonOffset;
}
renderScreen();
// touch clock, right side, reset offset
} else {
lonOffset = defLonOffset;
renderScreen();
}
});
renderAndQueue();

BIN
apps/dwm-clock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,19 @@
{
"id": "dwm-clock",
"name": "Daylight World Map Clock",
"shortName": "DWM Clock",
"version": "0.01",
"description": "A clock with a daylight world map",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"dwm-clock.app.js","url":"app.js"},
{"name":"dwm-clock.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"dwm-clock.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -128,7 +128,6 @@ LED.set();NRF.sleep();`);
posteditor.on("change", editorChanged);
document.getElementById("upload").addEventListener("click", function() {
if (!hasWarnings()) {
var precode = preeditor.getValue();
var jscode = jseditor.getValue();
var postcode = posteditor.getValue();
@ -159,7 +158,6 @@ LED.set();NRF.sleep();`);
})}]
});
});
}
});
document.getElementById("setdefault").addEventListener("click", function(e) {
e.preventDefault();

View File

@ -83,11 +83,8 @@
function onInit() {
Util.showModal("Loading...");
Util.readStorage("espruinoterm.json", function(j) {
Util.readStorageJSON("espruinoterm.json", function(options) {
Util.hideModal();
try {
options = JSON.parse(j);
} catch (e) {}
if (!Array.isArray(options)) setDefaults();
refresh();
});

View File

@ -17,7 +17,7 @@ This allows fast loading of all apps with two conditions:
## App history
* Long press of hardware button clears the app history and loads the clock face
* Installing the 'Fast Reset' app allows doing fastloads directly to the clock face by pressing the hardware button for one second. Useful if there are many apps in the history and the user want to access the clock quickly.
* Installing the 'Fast Reset' app allows doing fastloads directly to the clock face by pressing the hardware button just a little longer than a click. Useful if there are many apps in the history and the user want to access the clock quickly.
## Technical infos

View File

@ -1,2 +1,5 @@
0.01: New App!
0.02: Shorten the timeout before executing to 250 ms.
0.03: Add inner timeout of 150 ms so user has more time to release the button
before clock ui is initialized and adds it's button watch for going to
launcher.

View File

@ -1,5 +1,5 @@
{let buzzTimeout;
setWatch((e)=>{
if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);Bangle.showClock();}, 250);
if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);setTimeout(Bangle.showClock,150);}, 250);
if (!e.state && buzzTimeout) clearTimeout(buzzTimeout);},
BTN,{repeat:true,edge:'both'});}

View File

@ -1,7 +1,7 @@
{ "id": "fastreset",
"name": "Fast Reset",
"shortName":"Fast Reset",
"version":"0.02",
"version":"0.03",
"description": "Reset the watch by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.",
"icon": "app.png",
"type": "bootloader",

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///x3ygHo8H1Jf8AgILLoALVgoLHggLCqAgJioLIqgLDGQsBqtAioOBqoYFqtUAIMVBY9VqwCBDIIAECYILCHowLBrWVBZFlrWWyptGgtq1WqJYI7GrQLCFxGrBYJHEBQNV1Wv9IEBEocFKIOq//qJAIZEAoNq3/+1QMBHoYYBrQLB1J4GitaEYZfGtfvBYJ3HtWr9WlNY0V1Nr1WlC4xIBrWmBZWVrJGFcYILBZY4LBoILIgoNBEILvHDIQ5BBY4IBBYMBMAwLBBA4LPBRMAKAoLRiALWAGw="))

59
apps/flashcount/app.js Normal file
View File

@ -0,0 +1,59 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showMessage("Loading...");
Bangle.setOptions({hrmPollInterval:5});
Bangle.setHRMPower(1);
function drawCounter() {
g.reset().clearRect(0,24,175,90);
//g.drawRect(0,24,175,90);
g.setFontAlign(0,0).setFontVector(60);
g.drawString(count, 88, 60);
}
function hadPulse() {
count++;
drawCounter();
g.setColor("#f00").fillCircle(156,156,20);
setTimeout(function() {
g.setColor(g.theme.bg).fillCircle(156,156,20);
}, 500);
}
if (parseFloat(process.env.VERSION.replace("v","0"))<2019) {
E.showMessage("You need at least firmware 2v19","Error");
} else if (Bangle.hrmRd(0)!=33) { // wrong sensor - probably VC31 from original bangle.js 2
E.showMessage("This Bangle.js doesn't have a VC31B HRM sensor","Error");
} else {
Bangle.setOptions({hrmGreenAdjust:false, hrmWearDetect:false, hrmPushEnv:true});
Bangle.hrmWr(0x10, 197&0xF8 | 4); // just SLOT2
Bangle.hrmWr(0x16, 0); // force env to be used as fast as possible
var samples = 0, samplesHi = 0;
var count = 0;
{
let last = 0;
Bangle.on('HRM-env',v => {
if (v) {
if (!last) hadPulse();
samplesHi++;
}
last = v;
samples++;
});
}
drawCounter();
setInterval(function() {
g.reset().clearRect(0,90,175,130);
g.setFontAlign(0,0).setFont("6x8:2");
g.drawString(samples+" sps", 88, 100);
if (samplesHi*5 > samples) {
g.setBgColor("#f00").setColor("#fff");
g.clearRect(0,110,175,130).drawString("TOO LIGHT",88,120);
}
samples=0;
samplesHi=0;
Bangle.setLCDPower(1); // force LCD on!
}, 1000);
}

BIN
apps/flashcount/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,13 @@
{ "id": "flashcount",
"name": "Flash Counter",
"shortName":"FlashCount",
"version":"0.01",
"description": "Count flashes/pulses of light using the heart rate monitor. Requires a VC31B HRM sensor, which should be in most watches except those produced for the original KickStarter campaign.",
"icon": "app.png",
"tags": "",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"flashcount.app.js","url":"app.js"},
{"name":"flashcount.img","url":"app-icon.js","evaluate":true}
]
}

1
apps/forge/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: attempt to import

8
apps/forge/README.md Normal file
View File

@ -0,0 +1,8 @@
# App Forge
This should help with your hacks. Sometimes, you want to work on an
application, you'd want to use the stable version, but you'd also want
to use latest development version.
Well, this makes it easy. Just save your development version as
a.name.js, and you should be able to run it from the menu system.

1
apps/forge/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIhe/AEDgOKAocDwgFDgUEAokKAokTAohDEg0hgEgAoMEoMIoAFCgME4AFCwUCwAFBgoeChEAg8GAoMYEYMECIM4AoMMgFAuEAhv4gkg+EAhPghExAoIACg4FEh4FEj4FEn56Ev/8iAFC///CQUBAoPgQoQFBLYUHAoJbCh4FBFwf//wuD//8Fwf/GoYuNAoUGGggMCGgQeCbIl+Aol8Aol4Aoh2EgFgf5kAA"))

BIN
apps/forge/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

32
apps/forge/forge.app.js Normal file
View File

@ -0,0 +1,32 @@
// App Forge
st = require('Storage');
l = /^a\..*\.js$/;
//l = /.*\.js/;
l = st.list(l, {sf:false});
print(l);
function on_load(x) {
print("Loading", x);
Bangle.buzz(50, 1); // Won't happen because load() is quicker
g.reset().clear()
.setFont("Vector", 40)
.drawString("Loading", 0, 30)
.drawString(x, 0, 80);
g.flip();
load(x);
}
var menu = {
"< Back" : Bangle.load
};
if (l.length==0) Object.assign(menu, {"No apps":""});
else for (let id in l) {
let i = id;
menu[l[id]]=()=>{ on_load(l[i]); };
}
g.clear();
E.showMenu(menu);

13
apps/forge/metadata.json Normal file
View File

@ -0,0 +1,13 @@
{ "id": "forge",
"name": "App Forge",
"version":"0.01",
"description": "Easy way to run development versions of your apps",
"icon": "app.png",
"readme": "README.md",
"supports" : ["BANGLEJS2"],
"tags": "tool",
"storage": [
{"name":"forge.app.js","url":"forge.app.js"},
{"name":"forge.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -12,7 +12,8 @@
<a href="https://www.espruino.com/Bangle.js#firmware-updates" target="_blank">see the Bangle.js 1 instructions</a></b></p>
</div>
<ul>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span></p>
<p>Your current firmware version is <span id="fw-version" style="font-weight:bold">unknown</span> and DFU is <span id="boot-version" style="font-weight:bold">unknown</span>.
The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.</p>
</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

View File

@ -8,7 +8,7 @@
"tags": "tool,outdoors,gps",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"dependencies" : { "waypoints":"type" },
"dependencies" : { "waypoints":"app" },
"storage": [
{"name":"gpsnav.app.js","url":"app.min.js","supports":["BANGLEJS"]},
{"name":"gpsnav.app.js","url":"app_b2.js","supports":["BANGLEJS2"]},

View File

@ -8,7 +8,7 @@
"tags": "tool,outdoors,gps",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"dependencies" : { "waypoints":"type" },
"dependencies" : { "waypoints":"app" },
"interface" : "interface.html",
"storage": [
{"name":"gpstrek.app.js","url":"app.js"},

View File

@ -2,3 +2,4 @@
0.02: Refactor code to store grocery list in separate file
0.03: Sort selected items to bottom and enable Widgets
0.04: Add settings to edit list on device
0.05: Drop app customiser as it is redundant with download interface

View File

@ -1,18 +1,19 @@
var filename = 'grocery_list.json';
var settings = require("Storage").readJSON(filename,1)|| { products: [] };
{
const filename = 'grocery_list.json';
const settings = require("Storage").readJSON(filename,1)|| { products: [] };
let menu;
function updateSettings() {
const updateSettings = function() {
require("Storage").writeJSON(filename, settings);
Bangle.buzz();
}
};
function twoChat(n){
const twoChat = function(n) {
if(n<10) return '0'+n;
return ''+n;
}
};
function sortMenu() {
const sortMenu = function() {
mainMenu.sort((a,b) => {
const byValue = a.value-b.value;
return byValue !== 0 ? byValue : a.index-b.index;
@ -20,7 +21,7 @@ function sortMenu() {
if (menu) {
menu.draw();
}
}
};
const mainMenu = settings.products.map((p,i) => ({
title: twoChat(p.quantity)+' '+p.name,
@ -35,9 +36,14 @@ const mainMenu = settings.products.map((p,i) => ({
}));
sortMenu();
mainMenu[''] = { 'title': 'Grocery list' };
mainMenu[''] = {
'title': 'Grocery list',
remove: () => {
},
};
mainMenu['< Back'] = ()=>{load();};
Bangle.loadWidgets();
menu = E.showMenu(mainMenu);
Bangle.drawWidgets();
}

View File

@ -1,116 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<h4>List of products</h4>
<table class="table">
<thead>
<tr>
<th>name</th>
<th>quantity</th>
<th>actions</th>
</tr>
</thead>
<tbody id="products">
</tbody>
</table>
<br><br>
<h4>Add a new product</h4>
<form id="add_product_form">
<div class="columns">
<div class="column col-4 col-xs-12">
<input class="form-input input-sm" type="text" id="add_product_name" placeholder="Name">
</div>
<div class="column col-4 col-xs-12">
<input class="form-input input-sm" value="1" type="number" id="add_product_quantity" placeholder="Quantity">
</div>
<div class="column col-4 col-xs-12">
<button id="add_product_button" class="btn btn-primary btn-sm">Add</button>
</div>
</div>
</form>
<br><br>
<button id="reset" class="btn btn-error">Reset</button> <button id="upload" class="btn btn-primary">Upload</button>
<script src="../../core/lib/customize.js"></script>
<script>
var products = []
try{
var stored = localStorage.getItem('grocery-product-list')
if(stored) products = JSON.parse(stored);
}catch(e){}
var $name = document.getElementById('add_product_name')
var $form = document.getElementById('add_product_form')
var $button = document.getElementById('add_product_button')
var $quantity = document.getElementById('add_product_quantity')
var $list = document.getElementById('products')
var $reset = document.getElementById('reset')
renderProducts()
$reset.addEventListener('click', reset)
$form.addEventListener('submit', event => {
event.preventDefault()
var name = $name.value.trim()
if(!name) return;
var quantity = parseInt($quantity.value)
products.push({
name, quantity,
ok: false
})
renderProducts()
$name.value = ''
$quantity.value = 1
save()
})
function save(){
localStorage.setItem('grocery-product-list',JSON.stringify(products));
}
function reset(){
products = []
save()
renderProducts()
}
function removeProduct(index){
products = products.filter((p,i) => i!==index)
save()
renderProducts()
}
function renderProducts(){
$list.innerHTML = ''
products.forEach((product,index) => {
var $product = document.createElement('tr')
$product.innerHTML = `<td>${product.name}</td><td>${product.quantity}</td><td><button class="btn btn-error" onclick="removeProduct(${index})">remove</button></td>`
$list.appendChild($product)
})
$name.focus()
}
document.getElementById("upload").addEventListener("click", function() {
sendCustomizedApp({
storage:[
{ name:"grocery_list.json", content: JSON.stringify({products: products}) }
]
});
});
</script>
</body>
</html>

View File

@ -76,11 +76,11 @@
function getData() {
// show loading window
Util.showModal("Loading...");
Util.readStorage('grocery_list.json', data=>{
Util.readStorageJSON('grocery_list.json', data=>{
// remove window
Util.hideModal();
settings = JSON.parse(data || "{products: []}");
settings = data || {"products": []};
products = settings.products;
renderProducts();
});
@ -89,7 +89,6 @@
function save(){
settings.products = products;
Util.showModal("Saving...");
localStorage.setItem('grocery-product-list',JSON.stringify(products));
Util.writeStorage("grocery_list.json", JSON.stringify(settings), () => {
Util.hideModal();
});

View File

@ -1,13 +1,12 @@
{
"id": "grocery",
"name": "Grocery",
"version": "0.04",
"version": "0.05",
"description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.",
"icon": "grocery.png",
"type": "app",
"tags": "tool,shopping,list",
"supports": ["BANGLEJS", "BANGLEJS2"],
"custom": "grocery.html",
"interface": "interface.html",
"allow_emulator": true,
"dependencies": {"textinput":"type"},

View File

@ -118,7 +118,7 @@
/*LANG*/"Edit List": () => editlist(),
/*LANG*/"Add item": () => {
settings.products.push({
"name":/*LANG*/"New item",
"name":/*LANG*/"New",
"quantity":1,
"ok":false
});

View File

@ -211,9 +211,9 @@
this.editSong = song;
},
loadSongs: function () {
Util.readStorage('guitar_songs.json', (contents) => {
Util.readStorageJSON('guitar_songs.json', (contents) => {
this.songsState = 'loaded';
this.localSongs = JSON.parse(contents) || [];
this.localSongs = contents || [];
this.watchSongs = JSON.parse(JSON.stringify(this.localSongs));
});
window.setTimeout(() => {

View File

@ -2,3 +2,4 @@
0.02: Respect Quiet Mode
0.03: Fix hour/minute wrapping code for new menu system
0.04: Use default Bangle formatter for booleans
0.05: Support BangleJS 2

16
apps/hardalarm/README.md Normal file
View File

@ -0,0 +1,16 @@
Hard Alarm
========
The Hard Alarm will is vibration only alarm clock that will not stop vibrating until you are awake enough to match the random number on the screen.
To set a hard alarm open the app and in the settings screen:
1. Press the top half of the screen add 1 and press the bottom half of the screen to subtract 1.
2. Press the center number itself to save your selection
3. Exit the app or it may not run the alarm.
When the alarm goes off:
1. Press the top half of the screen add 1 and press the bottom half of the screen to subtract 1.
2. Press the side button to confirm your selection and turn off the alarm
3. You now have the touchscreen option to Snooze the alarm for 10 more minutes or end the alarm.
Made by https://dare.fail

View File

@ -8,7 +8,6 @@ var alarms = require("Storage").readJSON("hardalarm.json",1)||[];
msg : "Eat chocolate",
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
rp : true, // repeat
as : false, // auto snooze
}
];*/
@ -45,14 +44,12 @@ function editAlarm(alarmIndex) {
var mins = 0;
var en = true;
var repeat = true;
var as = false;
if (!newAlarm) {
var a = alarms[alarmIndex];
hrs = 0|a.hr;
mins = Math.round((a.hr-hrs)*60);
en = a.on;
repeat = a.rp;
as = a.as;
}
const menu = {
'': { 'title': 'Alarms' },
@ -72,10 +69,6 @@ function editAlarm(alarmIndex) {
value: en,
onchange: v=>repeat=v
},
/*LANG*/'Auto snooze': {
value: as,
onchange: v=>as=v
}
};
function getAlarm() {
var hr = hrs+(mins/60);
@ -86,7 +79,7 @@ function editAlarm(alarmIndex) {
// Save alarm
return {
on : en, hr : hr,
last : day, rp : repeat, as: as
last : day, rp : repeat
};
}
menu["> Save"] = function() {

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