Tasks tab: NE Hub lists plus iCloud reminder lists where Apple exposes them
build-container / build (push) Successful in 1m43s
build-container / build (push) Successful in 1m43s
- Lists, quick add, due dates, notes, complete/reopen, delete; Home card for overdue/due today - iCloud VTODO lists read and written over CalDAV (If-Match, edits patch the latest copy so priority/alarms survive); NE Hub lists work regardless - Fix: text inside a nested VALARM was read as the task's / event's own description and then written back on edit; nested components are now skipped (regression tests added) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c7917e57c
commit
916882e55d
9 files changed
+307
-15
No files matched your search
@@ -162,6 +162,50 @@ function fieldsFromEvent(ev, clientTz) {
|
||||
return { ...base, allDay: false, start: wallClock(s.ms, tz), end: wallClock(endMs, tz), tz };
|
||||
}
|
||||
|
||||
|
||||
// ---- Tasks (VTODO). fields: { title, notes, due: '' | 'YYYY-MM-DD', done: boolean }
|
||||
function validateTodo(f) {
|
||||
if (!f || !String(f.title || '').trim()) throw new Error('Give the task a title');
|
||||
if (f.due && !/^\d{4}-\d{2}-\d{2}$/.test(f.due)) throw new Error('The due date is not valid');
|
||||
}
|
||||
function todoLines(f, { completed = true } = {}) {
|
||||
const L = ['SUMMARY:' + esc(String(f.title).trim())];
|
||||
if (f.notes) L.push('DESCRIPTION:' + esc(f.notes));
|
||||
if (f.due) L.push('DUE;VALUE=DATE:' + f.due.replace(/-/g, ''));
|
||||
if (f.done) { L.push('STATUS:COMPLETED', 'PERCENT-COMPLETE:100'); if (completed) L.push('COMPLETED:' + nowStamp()); }
|
||||
else L.push('STATUS:NEEDS-ACTION');
|
||||
return L;
|
||||
}
|
||||
function buildTodoCalendar(items) {
|
||||
const L = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//NE Hub//Tasks//EN'];
|
||||
for (const i of items) { validateTodo(i.fields); L.push('BEGIN:VTODO', 'UID:' + i.uid, 'DTSTAMP:' + nowStamp(), 'CREATED:' + nowStamp(), 'SEQUENCE:0', ...todoLines(i.fields), 'END:VTODO'); }
|
||||
L.push('END:VCALENDAR');
|
||||
return L.map(fold).join('\r\n') + '\r\n';
|
||||
}
|
||||
/** Edit an existing VTODO resource in place; alarms and other properties are kept. */
|
||||
function patchTodo(text, f) {
|
||||
validateTodo(f);
|
||||
const lines = text.replace(/\r?\n[ \t]/g, '').split(/\r?\n/).filter(l => l !== '');
|
||||
const a = lines.indexOf('BEGIN:VTODO'), b = lines.indexOf('END:VTODO');
|
||||
if (a < 0 || b < a) throw new Error('Could not find the task in that entry');
|
||||
const out = []; let depth = 0, seq = 0, hadCompleted = false, sawStamp = false;
|
||||
lines.forEach((l, i) => {
|
||||
if (i < a || i > b) { out.push(l); return; }
|
||||
if (i === a) { out.push(l); return; }
|
||||
if (i === b) { out.push(...todoLines(f, { completed: !hadCompleted }), 'SEQUENCE:' + (seq + 1)); if (!sawStamp) out.push('DTSTAMP:' + nowStamp()); out.push(l); return; }
|
||||
if (/^BEGIN:/.test(l)) { depth++; out.push(l); return; }
|
||||
if (/^END:/.test(l)) { depth--; out.push(l); return; }
|
||||
if (depth > 0) { out.push(l); return; }
|
||||
const name = l.split(/[;:]/)[0].toUpperCase();
|
||||
if (name === 'COMPLETED') { if (f.done) { hadCompleted = true; out.push(l); } return; } // keep the original completion time
|
||||
if (['SUMMARY', 'DESCRIPTION', 'DUE', 'STATUS', 'PERCENT-COMPLETE'].includes(name)) return;
|
||||
if (name === 'SEQUENCE') { seq = parseInt(l.split(':')[1], 10) || 0; return; }
|
||||
if (name === 'DTSTAMP') { out.push('DTSTAMP:' + nowStamp()); sawStamp = true; return; }
|
||||
out.push(l);
|
||||
});
|
||||
return out.map(fold).join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
const newUid = () => crypto.randomUUID() + '@nehub';
|
||||
|
||||
module.exports = { buildVevent, buildCalendar, patchIcs, fieldsFromEvent, vtimezone, validate, newUid, wallClock, validTz };
|
||||
module.exports = { validateTodo, buildTodoCalendar, patchTodo, buildVevent, buildCalendar, patchIcs, fieldsFromEvent, vtimezone, validate, newUid, wallClock, validTz };
|
||||
Reference in new issue
Block a user