ICS Calendar Export Plugin

name

ICS Calendar Export

description

Export the scheduled tasks in a specific task domain to an ICS file that can be imported in various calendar apps and services.


{
async appOption(app) {
const taskDomains = await app.getTaskDomains();
 
const taskDomainUUID = await app.prompt("Select a task domain", {
inputs: [
{
options: taskDomains.map(taskDomain => ({
label: taskDomain.name,
value: taskDomain.uuid,
})),
type: "select",
}
]
});
if (!taskDomainUUID) return;
 
const now = Math.floor(Date.now() / 1000);
 
const lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Amplenote//CalDAV Client//EN",
];
 
for await (const task of app.getTaskDomainTasks(taskDomainUUID)) {
if (!task.startAt) continue;
 
lines.push("BEGIN:VEVENT");
lines.push(`DTSTART:${ this._formatDateTime(new Date(task.startAt * 1000)) }`);
 
const endAt = task.endAt || (task.startAt + 15 * 60);
lines.push(`DTEND:${ this._formatDateTime(new Date(endAt * 1000)) }`);
 
if (task.repeat) {
const [ _dtstart, rrule ] = task.repeat.split("\n");
if (rrule.startsWith("RRULE:")) lines.push(rrule);
}
 
lines.push(`UID:${ task.uuid }`);
 
// All these are attempts to get external calendars to update the instance if the user re-imports an
// updated ics, however as of 10/2023 this is still not sufficient to get Apple Calendar to update an
// event.
lines.push("METHOD:REQUEST");
lines.push(`SEQUENCE:${ now }`);
lines.push("ORGANIZER:Amplenote");
 
lines.push(`SUMMARY:${ this._escapeICalValue(task.content) }`);
lines.push(`URL;VALUE=URI:${ this._escapeICalValue("https://www.amplenote.com/notes/" + task.noteUUID) }`);
lines.push("END:VEVENT");
}
 
lines.push("END:VCALENDAR");
 
const ical = this._foldICalLines(lines);
 
const file = new Blob([ ical ], { type: "text/calendar" });
await app.saveFile(file, "tasks.ics");
},
 
// --------------------------------------------------------------------------
// Based on https://github.com/sebbo2002/ical-generator/blob/develop/src/tools.ts#L117
_escapeICalValue(value) {
return value.replace(/[\\;,]/g, match => `\\${ match }`).replace(/(?:\r\n|\r|\n)/g, '\\n');
},
 
// --------------------------------------------------------------------------
// From https://github.com/sebbo2002/ical-generator/blob/develop/src/tools.ts#L126
_foldICalLines(lines) {
return lines.map(function(line) {
let result = '';
let c = 0;
for (let i = 0; i < line.length; i++) {
let ch = line.charAt(i);
 
// surrogate pair, see https://mathiasbynens.be/notes/javascript-encoding#surrogate-pairs
if (ch >= '\ud800' && ch <= '\udbff') {
ch += line.charAt(++i);
}
 
// TextEncoder is available in browsers and node.js >= 11.0.0
const charsize = new TextEncoder().encode(ch).length;
c += charsize;
if (c > 74) {
result += '\r\n ';
c = charsize;
}
 
result += ch;
}
return result;
}).join('\r\n');
},
 
// --------------------------------------------------------------------------
_formatDateTime(date) {
const year = date.getUTCFullYear();
const month = this._pad(date.getUTCMonth() + 1);
const day = this._pad(date.getUTCDate());
const hour = this._pad(date.getUTCHours());
const minute = this._pad(date.getUTCMinutes());
const second = this._pad(date.getUTCSeconds());
return `${ year }${ month }${ day }T${ hour }${ minute }${ second }Z`;
},
 
// --------------------------------------------------------------------------
_pad(i) {
return i < 10 ? `0${i}` : `${i}`;
},
}