1
0
Fork 0

Merge branch 'espruino:master' into master

master
stweedo 2023-05-09 04:49:59 -05:00 committed by GitHub
commit ef29ff3709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 338 additions and 98 deletions

View File

@ -13,3 +13,4 @@
0.12: Mark dated events on a day
0.13: Switch to swipe left/right for month and up/down for year selection
Display events for current month on touch
0.14: Add support for holidays

View File

@ -10,6 +10,7 @@ Basic calendar
- Swipe down (Bangle.js 2 only) to go to the next year
- Touch to display events for current month
- Press the button (button 3 on Bangle.js 1) to exit
- Holidays have same color as weekends and can be edited with the 'Download'-interface, e.g. by uploading an iCalendar file.
## Settings

View File

@ -16,6 +16,7 @@ const white = "#ffffff";
const red = "#d41706";
const blue = "#0000ff";
const yellow = "#ffff00";
const cyan = "#00ffff";
let bgColor = color4;
let bgColorMonth = color1;
let bgColorDow = color2;
@ -23,6 +24,7 @@ let bgColorWeekend = color3;
let fgOtherMonth = gray1;
let fgSameMonth = white;
let bgEvent = blue;
let bgOtherEvent = "#ff8800";
const eventsPerDay=6; // how much different events per day we can display
const date = new Date();
@ -36,9 +38,17 @@ const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a
date.setHours(time.h);
date.setMinutes(time.m);
date.setSeconds(time.s);
return {date: date, msg: a.msg};
return {date: date, msg: a.msg, type: "e"};
});
// add holidays & other events
(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
const date = new Date(d.date);
const o = {date: date, msg: d.name, type: d.type};
if (d.repeat) {
o.repeat = d.repeat;
}
events.push(o);
});
events.sort((a,b) => a.date - b.date);
if (settings.ndColors === undefined) {
settings.ndColors = !g.theme.dark;
@ -52,68 +62,16 @@ if (settings.ndColors === true) {
fgOtherMonth = blue;
fgSameMonth = black;
bgEvent = color2;
bgOtherEvent = cyan;
}
function getDowLbls(locale) {
let dowLbls;
//TODO: Find some clever way to generate this programmatically from locale lib
switch (locale) {
case "de_AT":
case "de_CH":
case "de_DE":
if (startOnSun) {
dowLbls = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
} else {
dowLbls = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
}
break;
case "nl_NL":
if (startOnSun) {
dowLbls = ["zo", "ma", "di", "wo", "do", "vr", "za"];
} else {
dowLbls = ["ma", "di", "wo", "do", "vr", "za", "zo"];
}
break;
case "fr_BE":
case "fr_CH":
case "fr_FR":
if (startOnSun) {
dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
} else {
dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"];
}
break;
case "sv_SE":
if (startOnSun) {
dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
} else {
dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"];
}
break;
case "it_CH":
case "it_IT":
if (startOnSun) {
dowLbls = ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"];
} else {
dowLbls = ["Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"];
}
break;
case "oc_FR":
if (startOnSun) {
dowLbls = ["dg", "dl", "dm", "dc", "dj", "dv", "ds"];
} else {
dowLbls = ["dl", "dm", "dc", "dj", "dv", "ds", "dg"];
}
break;
default:
if (startOnSun) {
dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
} else {
dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
}
break;
}
return dowLbls;
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) {
@ -206,8 +164,13 @@ function drawCalendar(date) {
weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7);
const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0);
week2AfterMonth.setDate(week2AfterMonth.getDate() + 14);
events.forEach(ev => {
if (ev.repeat === "y") {
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);
let i = 0;
for (y = 0; y < rowN - 1; y++) {
for (x = 0; x < colN; x++) {
@ -220,6 +183,33 @@ function drawCalendar(date) {
const y1 = y * rowH + headerH + rowH;
const x2 = x * colW + colW;
const y2 = y * rowH + headerH + rowH + rowH;
if (eventsThisMonth.length > 0) {
// Display events for this day
eventsThisMonth.forEach((ev, idx) => {
if (sameDay(ev.date, curDay)) {
switch(ev.type) {
case "e": // alarm/event
const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
const height = (y2-2) - (y1+2); // height of a cell
const sliceHeight = height/eventsPerDay;
const ystart = (y1+2) + slice*sliceHeight;
g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
break;
case "h": // holiday
g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
break;
case "o": // other
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
break;
}
eventsThisMonth.splice(idx, 1); // this event is no longer needed
}
});
}
if (isToday) {
g.setColor(red);
g.drawRect(x1, y1, x2, y2);
@ -231,23 +221,6 @@ function drawCalendar(date) {
);
}
if (eventsThisMonth.length > 0) {
// Display events for this day
g.setColor(bgEvent);
eventsThisMonth.forEach((ev, idx) => {
if (sameDay(ev.date, curDay)) {
const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
const height = (y2-2) - (y1+2); // height of a cell
const sliceHeight = height/eventsPerDay;
const ystart = (y1+2) + slice*sliceHeight;
g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
eventsThisMonth.splice(idx, 1); // this event is no longer needed
}
});
}
require("Font8x12").add(Graphics);
g.setFont("8x12", fontSize);
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
@ -286,10 +259,11 @@ function setUI() {
},
btn: (n) => n === (process.env.HWVERSION === 2 ? 1 : 3) && 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 => {
const dateStr = require("locale").date(e.date, 1);
const timeStr = require("locale").time(e.date, 1);
return { title: `${dateStr} ${timeStr}` + (e.msg ? " " + e.msg : "") };
return { title: `${dateStr} ${e.type === "e" ? timeStr : ""}` + (e.msg ? " " + e.msg : "") };
});
if (menu.length === 0) {
menu.push({title: /*LANG*/"No events"});

View File

@ -0,0 +1,234 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<script src="../../core/lib/interface.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ical.js/1.5.0/ical.min.js"></script>
<script>
let dataElement = document.getElementById("data");
let holidays;
function sameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
function render() {
holidays.sort((a,b) => new Date(a.date) - new Date(b.date));
document.getElementById('events').innerHTML = "";
holidays.forEach(holiday => {
renderHoliday(holiday);
});
}
function readFile(input) {
document.getElementById('upload').disabled = true;
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
const jCalData = ICAL.parse(reader.result);
const comp = new ICAL.Component(jCalData);
// Fetch the VEVENT part
comp.getAllSubcomponents('vevent').forEach(vevent => {
event = new ICAL.Event(vevent);
holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists
const holiday = eventToHoliday(event);
holidays.push(holiday);
});
render();
}, false);
reader.readAsText(input.files[i], "UTF-8");
}
}
function eventToHoliday(event) {
const date = event.startDate.toJSDate();
const holiday = {
date: formatDate(date),
name: event.summary,
type: 'h',
};
return holiday;
}
function formatDate(d) {
return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0');
}
function upload() {
Util.showModal("Saving...");
Util.writeStorage("calendar.days.json", JSON.stringify(holidays), () => {
location.reload(); // reload so we see current data
});
}
function renderHoliday(holiday) {
const localDate = new Date(holiday.date);
const tr = document.createElement('tr');
tr.classList.add('event-row');
const tdTime = document.createElement('td');
tr.appendChild(tdTime);
const inputTime = document.createElement('input');
inputTime.type = "date";
inputTime.classList.add('event-date');
inputTime.classList.add('form-input');
inputTime.value = formatDate(localDate)
inputTime.onchange = (e => {
const date = new Date(inputTime.value);
holiday.date = formatDate(date);
});
tdTime.appendChild(inputTime);
const tdSummary = document.createElement('td');
tr.appendChild(tdSummary);
const inputSummary = document.createElement('input');
inputSummary.type = "text";
inputSummary.classList.add('event-summary');
inputSummary.classList.add('form-input');
inputSummary.maxLength=40;
const summary = (holiday.name?.substring(0, inputSummary.maxLength) || "");
inputSummary.value = summary;
inputSummary.onchange = (e => {
holiday.name = inputSummary.value;
});
tdSummary.appendChild(inputSummary);
inputSummary.onchange();
const tdType = document.createElement('td');
tr.appendChild(tdType);
const selectType = document.createElement("select");
selectType.classList.add('form-select');
tdType.prepend(selectType);
const optionHoliday = document.createElement("option");
optionHoliday.text = "Holiday";
optionHoliday.value = "h";
optionHoliday.selected = holiday.type === "h";
selectType.add(optionHoliday);
const optionOther = document.createElement("option");
optionOther.text = "Other";
optionOther.value = "o";
optionOther.selected = holiday.type === "o";
selectType.add(optionOther);
selectType.onchange = (e => {
holiday.type = e.target.value;
});
const tdRepeat = document.createElement('td');
tr.appendChild(tdRepeat);
const selectRepeat = document.createElement("select");
selectRepeat.classList.add('form-select');
tdRepeat.prepend(selectRepeat);
const optionNever = document.createElement("option");
optionNever.text = "Never";
optionNever.selected = !holiday.repeat;
selectRepeat.add(optionNever);
const optionYearly = document.createElement("option");
optionYearly.text = "Yearly";
optionYearly.value = "y";
optionYearly.selected = holiday.repeat === "y";
selectRepeat.add(optionYearly);
selectRepeat.onchange = (e => {
holiday.repeat = e.target.value;
});
const tdAction = document.createElement('td');
tr.appendChild(tdAction);
const buttonDelete = document.createElement('button');
buttonDelete.classList.add('btn');
buttonDelete.classList.add('btn-action');
tdAction.prepend(buttonDelete);
const iconDelete = document.createElement('i');
iconDelete.classList.add('icon');
iconDelete.classList.add('icon-delete');
buttonDelete.appendChild(iconDelete);
buttonDelete.onclick = (e => {
holidays = holidays.filter(a => a !== holiday);
document.getElementById('events').removeChild(tr);
});
document.getElementById('events').appendChild(tr);
document.getElementById('upload').disabled = false;
}
function addHoliday() {
const holiday = {date: formatDate(new Date()), type: 'h'};
renderHoliday(holiday);
holidays.push(holiday);
render();
}
function getData() {
Util.showModal("Loading...");
Puck.write(`\x10(function() {
Bluetooth.print(JSON.stringify(require("Storage").list("calendar.days.json").sort()));
})()\n`, contents => {
const fileNames = JSON.parse(contents);
if (fileNames.length > 0) {
Util.readStorage('calendar.days.json',data=>{
holidays = JSON.parse(data || "[]") || [];
Util.hideModal();
render();
});
} else {
holidays = [];
Util.hideModal();
}
});
}
// Called when app starts
function onInit() {
getData();
}
</script>
</head>
<body>
<h4 class="float-left">Holidays</h4>
<div class="float-right">
<button class="btn" onclick="addHoliday();">
<i class="icon icon-plus"></i>
</button>
</div>
<table class="table table-scroll" style="clear:both;">
<thead>
<tr>
<th>Date</th>
<th>Holiday</th>
<th>Type</th>
<th>Repeat</th>
<th></th>
</tr>
</thead>
<tbody id="events">
</tbody>
</table>
<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 iCalendar file</label>
</div>
<div class="col-7 col-xs-12">
<input id="fileinput" class="form-input" type="file" onchange="readFile(this)" accept=".ics,.ifb,.ical,.ifbf" multiple/>
</div>
</div>
</div>
<div class="divider"></div>
<button id="upload" class="btn btn-primary" onClick="upload()" disabled>Upload</button>
<button id="reload" class="btn" onClick="location.reload()">Reload</button>
</body>
</html>

View File

@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.13",
"version": "0.14",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
@ -9,10 +9,11 @@
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"interface": "interface.html",
"storage": [
{"name":"calendar.app.js","url":"calendar.js"},
{"name":"calendar.settings.js","url":"settings.js"},
{"name":"calendar.img","url":"calendar-icon.js","evaluate":true}
],
"data": [{"name":"calendar.json"}]
"data": [{"name":"calendar.json"}, {"name":"calendar.days.json"}]
}

View File

@ -9,4 +9,4 @@
0.07: Convert Yes/No On/Off in settings to checkboxes
0.08: Fix the wrapping of intervals/timeouts with parameters
Fix the widget drawing if widgets are hidden and Bangle.setLCDBrightness is called
0.09: Cache the app-launch info
0.09: Accidental version bump

View File

@ -1 +1,2 @@
0.01: Initial release
0.02: Cache the app-launch info

View File

@ -2,7 +2,7 @@
"id": "ratchet_launch",
"name": "Ratchet Launcher",
"shortName": "Ratchet",
"version": "0.01",
"version": "0.02",
"description": "Launcher with discrete scrolling for quicker app selection",
"icon": "app.png",
"type": "launch",

View File

@ -3,7 +3,7 @@
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<script src="../../core/lib/interface.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ical.js/0.0.3/ical.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ical.js/1.5.0/ical.min.js"></script>
<script>
let dataElement = document.getElementById("data");
let alarms;
@ -17,7 +17,7 @@ function readFile(input) {
const reader = new FileReader();
reader.addEventListener("load", () => {
const jCalData = ICAL.parse(reader.result);
const comp = new ICAL.Component(jCalData[1]);
const comp = new ICAL.Component(jCalData);
// Fetch the VEVENT part
comp.getAllSubcomponents('vevent').forEach(vevent => {
event = new ICAL.Event(vevent);
@ -50,13 +50,17 @@ function dateFromAlarm(alarm) {
return new Date(date.getTime() + alarm.t);
}
function formatDate(d) {
return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0');
}
function getAlarmDefaults() {
const date = new Date();
return {
on: true,
t: dateToMsSinceMidnight(date),
dow: 127,
date: date.toISOString().substring(0,10),
date: formatDate(date),
last: 0,
rp: "defaultRepeat" in schedSettings ? schedSettings.defaultRepeat : false,
vibrate: "defaultAlarmPattern" in schedSettings ? schedSettings.defaultAlarmPattern : "::",
@ -72,7 +76,7 @@ function eventToAlarm(event, offsetMs) {
id: event.uid,
msg: event.summary,
t: dateToMsSinceMidnight(date),
date: date.toISOString().substring(0,10),
date: formatDate(date),
data: {end: event.endDate.toJSDate().toISOString()}
}};
if (offsetMs) { // Alarm time is not real event time, so do a backup
@ -105,7 +109,7 @@ function renderAlarm(alarm, exists) {
inputTime.onchange = (e => {
const date = new Date(inputTime.value);
alarm.t = dateToMsSinceMidnight(date);
alarm.date = date.toISOString().substring(0,10);
alarm.date = formatDate(date);
});
tdTime.appendChild(inputTime);

View File

@ -1,3 +1,4 @@
0.01: New app!
0.02: Bug fixes
0.03: Submitted to the app loader
0.03: Submitted to the app loader
0.04: Reduce battery consumption when the user's not looking

View File

@ -8,7 +8,7 @@ const BUTTON_ICONS = {
reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI="))
};
let state = storage.readJSON(STATE_PATH);
let state = storage.readJSON(STATE_PATH, 1);
const STATE_DEFAULT = {
wasRunning: false, //If the stopwatch was ever running since being reset
sessionStart: 0, //When the stopwatch was first started
@ -157,7 +157,7 @@ function firstTimeStart(now, time) {
elapsedTime: 0,
};
lapFile = 'stlap-' + state.sessionStart + '.json';
setupTimerInterval();
setupTimerIntervalFast();
Bangle.buzz(200);
drawButtons();
}
@ -201,13 +201,15 @@ function start(now, time) {
state.elapsedTime += (state.pausedTime - state.startTime);
state.startTime = now;
state.running = true;
setupTimerInterval();
setupTimerIntervalFast();
Bangle.buzz(200);
drawTime();
drawButtons();
}
Bangle.on("touch", (button, xy) => {
setupTimerIntervalFast();
//In gesture mode, just turn on the light and then return
if (gestureMode) {
Bangle.setLCDPower(true);
@ -242,6 +244,8 @@ Bangle.on("touch", (button, xy) => {
});
Bangle.on('swipe', direction => {
setupTimerIntervalFast();
let now = (new Date()).getTime();
let time = getTime();
@ -272,12 +276,23 @@ setWatch(() => {
}, BTN1, { repeat: true });
let timerInterval;
let userWatching = false;
function setupTimerIntervalFast() {
userWatching = true;
setupTimerInterval();
setTimeout(() => {
userWatching = false;
setupTimerInterval();
}, 5000);
}
function setupTimerInterval() {
if (timerInterval !== undefined) {
clearInterval(timerInterval);
}
timerInterval = setInterval(drawTime, 10);
timerInterval = setInterval(drawTime, userWatching ? 10 : 1000);
}
function stopTimerInterval() {
@ -289,7 +304,7 @@ function stopTimerInterval() {
drawTime();
if (state.running) {
setupTimerInterval();
setupTimerIntervalFast();
}
//Save our state when the app is closed
@ -300,5 +315,8 @@ E.on('kill', () => {
}
});
// change interval depending of whether the user's looking
Bangle.on("twist", setupTimerIntervalFast);
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -1,7 +1,7 @@
{
"id": "stlap",
"name": "Stopwatch",
"version": "0.03",
"version": "0.04",
"description": "A stopwatch that remembers its state, with a lap timer and a gesture mode (enable by swiping)",
"icon": "icon.png",
"type": "app",
@ -20,5 +20,9 @@
"url": "icon.js",
"evaluate": true
}
],
"data": [
{"name":"stlap.state.json"},
{"wildcard":"stlap-*.json"}
]
}

View File

@ -1 +1,2 @@
0.01: Initial release
0.02: Disallow emulator

View File

@ -1,7 +1,7 @@
{
"id": "widtemp",
"name": "Temperature widget",
"version": "0.01",
"version": "0.02",
"description": "A temperature widget, showing the current internal temperature",
"readme": "README.md",
"icon": "widtemp.png",
@ -10,7 +10,7 @@
"tags": "widget,health",
"supports": ["BANGLEJS","BANGLEJS2"],
"dependencies" : {},
"allow_emulator":true,
"allow_emulator":false,
"storage": [
{"name":"widtemp.wid.js","url":"widtemp.wid.js"}
]