From dad4fc23047faa095bf486ad13f2c7173188ac47 Mon Sep 17 00:00:00 2001 From: Muness Castle <931+muness@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:19:05 -0500 Subject: [PATCH 1/3] WIP: Adding guests and their responses --- src/IEvent.ts | 7 ++ src/main.ts | 303 +++++++++++++++++++++++++++----------------------- 2 files changed, 169 insertions(+), 141 deletions(-) diff --git a/src/IEvent.ts b/src/IEvent.ts index c1c7d62..1b48fc4 100644 --- a/src/IEvent.ts +++ b/src/IEvent.ts @@ -11,4 +11,11 @@ export interface IEvent { location: string; // Physical location where the event takes place, if applicable callUrl: string; // URL for joining online meetings/calls associated with the event callType: string; // Type of online meeting (e.g., Zoom, Skype, etc.) + guests: IGuest[]; // Array of guests attending the event +} + +export interface IGuest { + name: string; // Name of the guest + email: string; // Email of the guest + status: string; // Participation status (e.g., "Accepted", "Declined") } diff --git a/src/main.ts b/src/main.ts index 3d2b3d6..68ff7a6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,160 +1,181 @@ import { - Editor, moment, - MarkdownView, Notice + Editor, moment, + MarkdownView, Notice } from 'obsidian'; import { - Calendar, - ICSSettings, - DEFAULT_SETTINGS, + Calendar, + ICSSettings, + DEFAULT_SETTINGS, } from "./settings/ICSSettings"; import ICSSettingsTab from "./settings/ICSSettingsTab"; import { - getDateFromFile + getDateFromFile } from "obsidian-daily-notes-interface"; import { - Plugin, - request + Plugin, + request } from 'obsidian'; import { parseIcs, filterMatchingEvents, extractMeetingInfo } from './icalUtils'; -import { IEvent } from './IEvent'; +import { IEvent, IGuest } from './IEvent'; export default class ICSPlugin extends Plugin { - data: ICSSettings; - - async addCalendar(calendar: Calendar): Promise { - this.data.calendars = { - ...this.data.calendars, - [calendar.icsName]: calendar - }; - await this.saveSettings(); - } - - async removeCalendar(calendar: Calendar) { - if (this.data.calendars[calendar.icsName]) { - delete this.data.calendars[calendar.icsName]; - } - await this.saveSettings(); - } - - formatEvent(e: IEvent): string { - const callLinkOrLocation = e.callType ? `[${e.callType}](${e.callUrl})` : e.location; - - // Conditionally format start and end time based on dataViewSyntax setting - const startTimeFormatted = this.data.format.dataViewSyntax ? `[startTime:: ${e.time}]` : `${e.time}`; - const endTimeFormatted = e.format.includeEventEndTime ? (this.data.format.dataViewSyntax ? `[endTime:: ${e.endTime}]` : `- ${e.endTime}`) : ''; - - // Combine all parts of the formatted event string - return [ - `- ${e.format.checkbox ? '[ ]' : ''}`, - startTimeFormatted, - endTimeFormatted, - e.format.icsName ? e.icsName : '', - e.format.summary ? e.summary : '', - e.format.location ? callLinkOrLocation : '', - e.format.description && e.description ? `\n\t- ${e.description}` : '', - ].filter(Boolean).join(' ').trim(); -} - - async getEvents(date: string) : Promise { - let events: IEvent[] = []; - let errorMessages: string[] = []; // To store error messages - - for (const calendar in this.data.calendars) { - const calendarSetting = this.data.calendars[calendar]; - let icsArray: any[] = []; - - try { - if (calendarSetting.calendarType === 'vdir') { - // Assuming you have a method to list files in a directory - const icsFiles = app.vault.getFiles().filter(f => f.extension == "ics" && f.path.startsWith(calendarSetting.icsUrl)); - for (const icsFile of icsFiles) { - const fileContent = await this.app.vault.read(icsFile); - icsArray = icsArray.concat(parseIcs(fileContent)); - } - } else { - // Existing logic for remote URLs - icsArray = parseIcs(await request({ url: calendarSetting.icsUrl })); - } - } catch (error) { - console.error(`Error processing calendar ${calendarSetting.icsName}: ${error}`); - errorMessages.push(`Error processing calendar "${calendarSetting.icsName}"`); - } - - var dateEvents; - - // Exception handling for parsing and filtering - try { - dateEvents = filterMatchingEvents(icsArray, date); - - } catch (filterError) { - console.error(`Error filtering events for calendar ${calendarSetting.icsName}: ${filterError}`); - errorMessages.push(`Error filtering events in calendar "${calendarSetting.icsName}"`); - } - - try { - dateEvents.forEach((e) => { - const { callUrl, callType } = extractMeetingInfo(e); - - let event: IEvent = { - utime: moment(e.start).format('X'), - time: moment(e.start).format(this.data.format.timeFormat), - endTime: moment(e.end).format(this.data.format.timeFormat), - icsName: calendarSetting.icsName, - summary: e.summary, - description: e.description, - format: calendarSetting.format, - location: e.location? e.location : null, - callUrl: callUrl, - callType: callType - }; - events.push(event); - }); - } catch (parseError) { - console.error(`Error parsing events for calendar ${calendarSetting.icsName}: ${parseError}`); - errorMessages.push(`Error parsing events in calendar "${calendarSetting.icsName}"`); - } - } - - // Notify the user if any errors were encountered - if (errorMessages.length > 0) { - const message = `Encountered ${errorMessages.length} error(s) while processing calendars:\n\n${errorMessages.join('\n')}\nSee console for details.`; - new Notice(message); - } - - return events; - } - - async onload() { - await this.loadSettings(); - this.addSettingTab(new ICSSettingsTab(this.app, this)); - this.addCommand({ - id: "import_events", - name: "import events", - editorCallback: async (editor: Editor, view: MarkdownView) => { - const fileDate = getDateFromFile(view.file, "day").format("YYYY-MM-DD"); - var events: any[] = await this.getEvents(fileDate); - - const mdArray = events.sort((a,b) => a.utime - b.utime).map(this.formatEvent, this); - editor.replaceRange(mdArray.join("\n"), editor.getCursor()); - } - }); - } - - onunload() { - return; - } - - async loadSettings() { - this.data = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.data); - } + data: ICSSettings; + + async addCalendar(calendar: Calendar): Promise { + this.data.calendars = { + ...this.data.calendars, + [calendar.icsName]: calendar + }; + await this.saveSettings(); + } + + async removeCalendar(calendar: Calendar) { + if (this.data.calendars[calendar.icsName]) { + delete this.data.calendars[calendar.icsName]; + } + await this.saveSettings(); + } + + formatEvent(e: IEvent): string { + const callLinkOrLocation = e.callType ? `[${e.callType}](${e.callUrl})` : e.location; + const guestListFormatted = e.guests.map(guest => `\t\t- ${guest.name} (${guest.email}): ${guest.status}`).join('\n'); + + // Conditionally format start and end time based on dataViewSyntax setting + const startTimeFormatted = this.data.format.dataViewSyntax ? `[startTime:: ${e.time}]` : `${e.time}`; + const endTimeFormatted = e.format.includeEventEndTime ? (this.data.format.dataViewSyntax ? `[endTime:: ${e.endTime}]` : `- ${e.endTime}`) : ''; + + // Combine all parts of the formatted event string + return [ + `- ${e.format.checkbox ? '[ ]' : ''}`, + startTimeFormatted, + endTimeFormatted, + e.format.icsName ? e.icsName : '', + e.format.summary ? e.summary : '', + e.format.location ? callLinkOrLocation : '', + e.format.description && e.description ? `\n\t- ${e.description}` : '', + e.guests.length > 0 ? `\n\t- Guests:\n${guestListFormatted}` : '' + ].filter(Boolean).join(' ').trim(); + } + + async getEvents(date: string): Promise { + let events: IEvent[] = []; + let errorMessages: string[] = []; // To store error messages + + for (const calendar in this.data.calendars) { + const calendarSetting = this.data.calendars[calendar]; + let icsArray: any[] = []; + + try { + if (calendarSetting.calendarType === 'vdir') { + // Assuming you have a method to list files in a directory + const icsFiles = app.vault.getFiles().filter(f => f.extension == "ics" && f.path.startsWith(calendarSetting.icsUrl)); + for (const icsFile of icsFiles) { + const fileContent = await this.app.vault.read(icsFile); + icsArray = icsArray.concat(parseIcs(fileContent)); + } + } else { + // Existing logic for remote URLs + icsArray = parseIcs(await request({ url: calendarSetting.icsUrl })); + } + } catch (error) { + console.error(`Error processing calendar ${calendarSetting.icsName}: ${error}`); + errorMessages.push(`Error processing calendar "${calendarSetting.icsName}"`); + } + + var dateEvents; + + // Exception handling for parsing and filtering + try { + dateEvents = filterMatchingEvents(icsArray, date); + + } catch (filterError) { + console.error(`Error filtering events for calendar ${calendarSetting.icsName}: ${filterError}`); + errorMessages.push(`Error filtering events in calendar "${calendarSetting.icsName}"`); + } + + try { + dateEvents.forEach((e) => { + const { callUrl, callType } = extractMeetingInfo(e); + + let guests: IGuest[] = []; + if (e.attendee) { + if (Array.isArray(e.attendee)) { + guests = e.attendee.map(att => { + return { + name: att.params.CN, + email: att.val.substring(7), // Remove 'mailto:' prefix + status: att.params.PARTSTAT + }; + }); + } else { + guests.push({ + name: e.attendee.params.CN, + email: e.attendee.val.substring(7), // Remove 'mailto:' prefix + status: e.attendee.params.PARTSTAT + }); + } + } + let event: IEvent = { + utime: moment(e.start).format('X'), + time: moment(e.start).format(this.data.format.timeFormat), + endTime: moment(e.end).format(this.data.format.timeFormat), + icsName: calendarSetting.icsName, + summary: e.summary, + description: e.description, + format: calendarSetting.format, + location: e.location ? e.location : null, + callUrl: callUrl, + callType: callType, + guests: guests + }; + events.push(event); + }); + } catch (parseError) { + console.error(`Error parsing events for calendar ${calendarSetting.icsName}: ${parseError}`); + errorMessages.push(`Error parsing events in calendar "${calendarSetting.icsName}"`); + } + } + + // Notify the user if any errors were encountered + if (errorMessages.length > 0) { + const message = `Encountered ${errorMessages.length} error(s) while processing calendars:\n\n${errorMessages.join('\n')}\nSee console for details.`; + new Notice(message); + } + + return events; + } + + async onload() { + await this.loadSettings(); + this.addSettingTab(new ICSSettingsTab(this.app, this)); + this.addCommand({ + id: "import_events", + name: "import events", + editorCallback: async (editor: Editor, view: MarkdownView) => { + const fileDate = getDateFromFile(view.file, "day").format("YYYY-MM-DD"); + var events: any[] = await this.getEvents(fileDate); + + const mdArray = events.sort((a, b) => a.utime - b.utime).map(this.formatEvent, this); + editor.replaceRange(mdArray.join("\n"), editor.getCursor()); + } + }); + } + + onunload() { + return; + } + + async loadSettings() { + this.data = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.data); + } } From 0d981e44a03ce945ca642f5a40581eca3fd311c1 Mon Sep 17 00:00:00 2001 From: Muness Castle <931+muness@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:48:08 -0500 Subject: [PATCH 2/3] guests -> attendees, add more fields from ics spec for attendees --- package-lock.json | 4 ++-- src/IEvent.ts | 11 ++++++----- src/main.ts | 39 ++++++++++++++++----------------------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3ff500..160a633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-ics", - "version": "1.5.2", + "version": "1.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-ics", - "version": "1.5.2", + "version": "1.5.3", "license": "MIT", "dependencies": { "moment-timezone": "^0.5.43", diff --git a/src/IEvent.ts b/src/IEvent.ts index 1b48fc4..f5b8b04 100644 --- a/src/IEvent.ts +++ b/src/IEvent.ts @@ -11,11 +11,12 @@ export interface IEvent { location: string; // Physical location where the event takes place, if applicable callUrl: string; // URL for joining online meetings/calls associated with the event callType: string; // Type of online meeting (e.g., Zoom, Skype, etc.) - guests: IGuest[]; // Array of guests attending the event + attendees: IAttendee[]; // Array of attendees } -export interface IGuest { - name: string; // Name of the guest - email: string; // Email of the guest - status: string; // Participation status (e.g., "Accepted", "Declined") +export interface IAttendee { + email: string; + name: string; + role: string; + status: string; // Participation status (accepted, declined, etc.) } diff --git a/src/main.ts b/src/main.ts index 68ff7a6..19a76f4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,7 +20,7 @@ import { request } from 'obsidian'; import { parseIcs, filterMatchingEvents, extractMeetingInfo } from './icalUtils'; -import { IEvent, IGuest } from './IEvent'; +import { IEvent, IAttendee } from './IEvent'; export default class ICSPlugin extends Plugin { data: ICSSettings; @@ -42,7 +42,13 @@ export default class ICSPlugin extends Plugin { formatEvent(e: IEvent): string { const callLinkOrLocation = e.callType ? `[${e.callType}](${e.callUrl})` : e.location; - const guestListFormatted = e.guests.map(guest => `\t\t- ${guest.name} (${guest.email}): ${guest.status}`).join('\n'); + const attendeeList = e.attendees.map(attendee => { + // Check if the name and the email are identical + const displayName = attendee.name === attendee.email + ? attendee.name // If identical, use only one of them + : `${attendee.name} (${attendee.email})`; // If not, use both + return `\t\t- ${displayName}: ${attendee.status}`; + }).join('\n'); // Conditionally format start and end time based on dataViewSyntax setting const startTimeFormatted = this.data.format.dataViewSyntax ? `[startTime:: ${e.time}]` : `${e.time}`; @@ -57,7 +63,7 @@ export default class ICSPlugin extends Plugin { e.format.summary ? e.summary : '', e.format.location ? callLinkOrLocation : '', e.format.description && e.description ? `\n\t- ${e.description}` : '', - e.guests.length > 0 ? `\n\t- Guests:\n${guestListFormatted}` : '' + e.attendees.length > 0 ? `\n\t- Attendees:\n${attendeeList}` : '' ].filter(Boolean).join(' ').trim(); } @@ -101,25 +107,7 @@ export default class ICSPlugin extends Plugin { dateEvents.forEach((e) => { const { callUrl, callType } = extractMeetingInfo(e); - let guests: IGuest[] = []; - if (e.attendee) { - if (Array.isArray(e.attendee)) { - guests = e.attendee.map(att => { - return { - name: att.params.CN, - email: att.val.substring(7), // Remove 'mailto:' prefix - status: att.params.PARTSTAT - }; - }); - } else { - guests.push({ - name: e.attendee.params.CN, - email: e.attendee.val.substring(7), // Remove 'mailto:' prefix - status: e.attendee.params.PARTSTAT - }); - } - } - let event: IEvent = { + const event: IEvent = { utime: moment(e.start).format('X'), time: moment(e.start).format(this.data.format.timeFormat), endTime: moment(e.end).format(this.data.format.timeFormat), @@ -130,7 +118,12 @@ export default class ICSPlugin extends Plugin { location: e.location ? e.location : null, callUrl: callUrl, callType: callType, - guests: guests + attendees: e.attendee ? (Array.isArray(e.attendee) ? e.attendee : [e.attendee]).map(attendee => ({ + name: attendee.params.CN, + email: attendee.val.substring(7), + status: attendee.params.PARTSTAT, + role: attendee.params.ROLE + })) : [] }; events.push(event); }); From f1ceb69cad9bf0212c822fd4832bdd176e3ec8b9 Mon Sep 17 00:00:00 2001 From: Muness Castle <931+muness@users.noreply.github.com> Date: Fri, 12 Jan 2024 21:03:41 -0500 Subject: [PATCH 3/3] Add showAttendees seting to calendar format --- src/main.ts | 2 +- src/settings/ICSSettings.ts | 2 + src/settings/ICSSettingsTab.ts | 818 +++++++++++++++++---------------- 3 files changed, 417 insertions(+), 405 deletions(-) diff --git a/src/main.ts b/src/main.ts index 19a76f4..37f74d3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -63,7 +63,7 @@ export default class ICSPlugin extends Plugin { e.format.summary ? e.summary : '', e.format.location ? callLinkOrLocation : '', e.format.description && e.description ? `\n\t- ${e.description}` : '', - e.attendees.length > 0 ? `\n\t- Attendees:\n${attendeeList}` : '' + e.format.showAttendees && e.attendees.length > 0 ? `\n\t- Attendees:\n${attendeeList}` : '' ].filter(Boolean).join(' ').trim(); } diff --git a/src/settings/ICSSettings.ts b/src/settings/ICSSettings.ts index 79f275a..a429646 100644 --- a/src/settings/ICSSettings.ts +++ b/src/settings/ICSSettings.ts @@ -17,6 +17,7 @@ export interface Calendar { summary: boolean; location: boolean; description: boolean; + showAttendees: boolean; } } @@ -28,6 +29,7 @@ export const DEFAULT_CALENDAR_FORMAT = { location: true, description: false, calendarType: 'remote', // Set the default type to 'remote' + showAttendees: false, }; export const DEFAULT_SETTINGS: ICSSettings = { diff --git a/src/settings/ICSSettingsTab.ts b/src/settings/ICSSettingsTab.ts index b727aaa..9efc051 100644 --- a/src/settings/ICSSettingsTab.ts +++ b/src/settings/ICSSettingsTab.ts @@ -1,197 +1,197 @@ import ICSPlugin from "../main"; import { - PluginSettingTab, - Setting, - App, - ButtonComponent, - Modal, - TextComponent, - DropdownComponent, + PluginSettingTab, + Setting, + App, + ButtonComponent, + Modal, + TextComponent, + DropdownComponent, } from "obsidian"; import { - Calendar, - DEFAULT_CALENDAR_FORMAT + Calendar, + DEFAULT_CALENDAR_FORMAT } from "./ICSSettings"; import moment = require("moment"); export function getCalendarElement( - icsName: string): HTMLElement { - let calendarElement, titleEl; - - calendarElement = createDiv({ - cls: `calendar calendar-${icsName}`, - }); - titleEl = calendarElement.createEl("summary", { - cls: `calendar-name ${icsName}`, - text: icsName - }); - - return calendarElement; + icsName: string): HTMLElement { + let calendarElement, titleEl; + + calendarElement = createDiv({ + cls: `calendar calendar-${icsName}`, + }); + titleEl = calendarElement.createEl("summary", { + cls: `calendar-name ${icsName}`, + text: icsName + }); + + return calendarElement; } export default class ICSSettingsTab extends PluginSettingTab { - plugin: ICSPlugin; - timeFormatExample = document.createElement('b'); - - constructor(app: App, plugin: ICSPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - // use this same format to create a description for the dataViewSyntax setting - private timeFormattingDescription(): DocumentFragment { - this.updateTimeFormatExample(); - - const descEl = document.createDocumentFragment(); - descEl.appendText('Time format for events. HH:mm is 00:15. hh:mma is 12:15am.'); - descEl.appendText(' For more syntax, refer to '); - descEl.appendChild(this.getMomentDocsLink()); - descEl.appendText('.'); - - descEl.appendChild(document.createElement('p')); - descEl.appendText('Your current time format syntax looks like this: '); - descEl.appendChild(this.timeFormatExample); - descEl.appendText('.'); - return descEl; - } - - private getMomentDocsLink(): HTMLAnchorElement { - const a = document.createElement('a'); - a.href = 'https://momentjs.com/docs/#/displaying/format/'; - a.text = 'format reference'; - a.target = '_blank'; - return a; - } - - private updateTimeFormatExample() { - this.timeFormatExample.innerText = moment(new Date()).format(this.plugin.data.format.timeFormat); - } - - private dataViewSyntaxDescription(): DocumentFragment { - const descEl = document.createDocumentFragment(); - descEl.appendText('Enable this option if you use the DataView plugin to query event start and end times.'); - return descEl; - } - - display(): void { - let { - containerEl - } = this; - - containerEl.empty(); - - const calendarContainer = containerEl.createDiv( - "ics-setting-calendar" - ); - - const calnedarSetting = new Setting(calendarContainer) - .setHeading().setName("Calendars"); - - new Setting(calendarContainer) - .setName("Add new") - .setDesc("Add a new calendar") - .addButton((button: ButtonComponent): ButtonComponent => { - let b = button - .setTooltip("Add Additional") - .setButtonText("+") - .onClick(async () => { - let modal = new SettingsModal(this.app, this.plugin); - - modal.onClose = async () => { - if (modal.saved) { - this.plugin.addCalendar({ - icsName: modal.icsName, - icsUrl: modal.icsUrl, - format: modal.format, - calendarType: modal.calendarType as 'remote' | 'vdir', - }); - this.display(); - } - }; - - modal.open(); - }); - - return b; - }); - - const additional = calendarContainer.createDiv("calendar"); - for (let a in this.plugin.data.calendars) { - const calendar = this.plugin.data.calendars[a]; - - let setting = new Setting(additional); - - let calEl = getCalendarElement( - calendar.icsName); - setting.infoEl.replaceWith(calEl); - - setting - .addExtraButton((b) => { - b.setIcon("pencil") - .setTooltip("Edit") - .onClick(() => { - let modal = new SettingsModal(this.app, this.plugin, calendar); - - modal.onClose = async () => { - if (modal.saved) { - this.plugin.removeCalendar(calendar); - this.plugin.addCalendar({ - icsName: modal.icsName, - icsUrl: modal.icsUrl, - format: modal.format, - calendarType: modal.calendarType as 'remote' | 'vdir', - }); - this.display(); - } - }; - - modal.open(); - }); - }) - .addExtraButton((b) => { - b.setIcon("trash") - .setTooltip("Delete") - .onClick(() => { - this.plugin.removeCalendar(calendar); - this.display(); - }); - }); - } - - const formatSetting = new Setting(containerEl) - .setHeading().setName("Output Format"); - - - let timeFormat: TextComponent; - const timeFormatSetting = new Setting(containerEl) - .setName("Time format") - .setDesc(this.timeFormattingDescription()) - .addText((text) => { - timeFormat = text; - timeFormat.setValue(this.plugin.data.format.timeFormat).onChange(async (v) => { - this.plugin.data.format.timeFormat = v; - this.updateTimeFormatExample(); - await this.plugin.saveSettings(); - }); - }); - - const dataViewSyntaxSetting = new Setting(containerEl) - .setName('DataView Metadata syntax for start and end times') - .setDesc(this.dataViewSyntaxDescription()) - .addToggle(toggle => toggle - .setValue(this.plugin.data.format.dataViewSyntax || false) - .onChange(async (v) => { - this.plugin.data.format.dataViewSyntax = v; - await this.plugin.saveSettings(); - })); - - // Sponsor link - Thank you! - const divSponsor = containerEl.createDiv(); - divSponsor.innerHTML = `

A scratch my own itch project by muness.
+ plugin: ICSPlugin; + timeFormatExample = document.createElement('b'); + + constructor(app: App, plugin: ICSPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + // use this same format to create a description for the dataViewSyntax setting + private timeFormattingDescription(): DocumentFragment { + this.updateTimeFormatExample(); + + const descEl = document.createDocumentFragment(); + descEl.appendText('Time format for events. HH:mm is 00:15. hh:mma is 12:15am.'); + descEl.appendText(' For more syntax, refer to '); + descEl.appendChild(this.getMomentDocsLink()); + descEl.appendText('.'); + + descEl.appendChild(document.createElement('p')); + descEl.appendText('Your current time format syntax looks like this: '); + descEl.appendChild(this.timeFormatExample); + descEl.appendText('.'); + return descEl; + } + + private getMomentDocsLink(): HTMLAnchorElement { + const a = document.createElement('a'); + a.href = 'https://momentjs.com/docs/#/displaying/format/'; + a.text = 'format reference'; + a.target = '_blank'; + return a; + } + + private updateTimeFormatExample() { + this.timeFormatExample.innerText = moment(new Date()).format(this.plugin.data.format.timeFormat); + } + + private dataViewSyntaxDescription(): DocumentFragment { + const descEl = document.createDocumentFragment(); + descEl.appendText('Enable this option if you use the DataView plugin to query event start and end times.'); + return descEl; + } + + display(): void { + let { + containerEl + } = this; + + containerEl.empty(); + + const calendarContainer = containerEl.createDiv( + "ics-setting-calendar" + ); + + const calnedarSetting = new Setting(calendarContainer) + .setHeading().setName("Calendars"); + + new Setting(calendarContainer) + .setName("Add new") + .setDesc("Add a new calendar") + .addButton((button: ButtonComponent): ButtonComponent => { + let b = button + .setTooltip("Add Additional") + .setButtonText("+") + .onClick(async () => { + let modal = new SettingsModal(this.app, this.plugin); + + modal.onClose = async () => { + if (modal.saved) { + this.plugin.addCalendar({ + icsName: modal.icsName, + icsUrl: modal.icsUrl, + format: modal.format, + calendarType: modal.calendarType as 'remote' | 'vdir', + }); + this.display(); + } + }; + + modal.open(); + }); + + return b; + }); + + const additional = calendarContainer.createDiv("calendar"); + for (let a in this.plugin.data.calendars) { + const calendar = this.plugin.data.calendars[a]; + + let setting = new Setting(additional); + + let calEl = getCalendarElement( + calendar.icsName); + setting.infoEl.replaceWith(calEl); + + setting + .addExtraButton((b) => { + b.setIcon("pencil") + .setTooltip("Edit") + .onClick(() => { + let modal = new SettingsModal(this.app, this.plugin, calendar); + + modal.onClose = async () => { + if (modal.saved) { + this.plugin.removeCalendar(calendar); + this.plugin.addCalendar({ + icsName: modal.icsName, + icsUrl: modal.icsUrl, + format: modal.format, + calendarType: modal.calendarType as 'remote' | 'vdir', + }); + this.display(); + } + }; + + modal.open(); + }); + }) + .addExtraButton((b) => { + b.setIcon("trash") + .setTooltip("Delete") + .onClick(() => { + this.plugin.removeCalendar(calendar); + this.display(); + }); + }); + } + + const formatSetting = new Setting(containerEl) + .setHeading().setName("Output Format"); + + + let timeFormat: TextComponent; + const timeFormatSetting = new Setting(containerEl) + .setName("Time format") + .setDesc(this.timeFormattingDescription()) + .addText((text) => { + timeFormat = text; + timeFormat.setValue(this.plugin.data.format.timeFormat).onChange(async (v) => { + this.plugin.data.format.timeFormat = v; + this.updateTimeFormatExample(); + await this.plugin.saveSettings(); + }); + }); + + const dataViewSyntaxSetting = new Setting(containerEl) + .setName('DataView Metadata syntax for start and end times') + .setDesc(this.dataViewSyntaxDescription()) + .addToggle(toggle => toggle + .setValue(this.plugin.data.format.dataViewSyntax || false) + .onChange(async (v) => { + this.plugin.data.format.dataViewSyntax = v; + await this.plugin.saveSettings(); + })); + + // Sponsor link - Thank you! + const divSponsor = containerEl.createDiv(); + divSponsor.innerHTML = `

A scratch my own itch project by muness.
Buy Me a Book ` - } + } } @@ -199,228 +199,238 @@ export default class ICSSettingsTab extends PluginSettingTab { class SettingsModal extends Modal { - plugin: ICSPlugin; - icsName: string = ""; - icsUrl: string = ""; - urlSetting: Setting; - urlText: TextComponent; - urlDropdown: DropdownComponent; - - saved: boolean = false; - error: boolean = false; - format: { - checkbox: boolean, - includeEventEndTime: boolean, - icsName: boolean, - summary: boolean, - location: boolean, - description: boolean - } = DEFAULT_CALENDAR_FORMAT; - calendarType: string; - constructor(app: App, plugin: ICSPlugin, setting?: Calendar) { - super(app); - this.plugin = plugin; - if (setting) { - this.icsName = setting.icsName; - this.icsUrl = setting.icsUrl; - this.format = setting.format || this.format // if format is undefined, use default - this.calendarType = setting.calendarType || 'remote'; - } - } - - - listIcsDirectories(): string[] { - const icsFiles = this.app.vault.getFiles().filter(f => f.extension === "ics"); - const directories = new Set(icsFiles.map(f => f.parent.path)); - return Array.from(directories); - } - - display() { - let { - contentEl - } = this; - - contentEl.empty(); - - const settingDiv = contentEl.createDiv(); - - let nameText: TextComponent; - const nameSetting = new Setting(settingDiv) - .setName("Calendar Name") - .addText((text) => { - nameText = text; - nameText.setValue(this.icsName).onChange(async (v) => { - this.icsName = v; - }); - }); - - const calendarTypeSetting = new Setting(settingDiv) - .setName('Calendar Type') - .setDesc('Select the type of calendar (Remote URL or vdir)') - .addDropdown(dropdown => { - dropdown.addOption('remote', 'Remote URL'); - dropdown.addOption('vdir', 'vdir'); - dropdown.setValue(this.calendarType) - .onChange(value => { - this.calendarType = value as 'remote' | 'vdir'; - updateUrlSetting(); - }); - }); - - const urlSettingDiv = settingDiv.createDiv({ cls: 'url-setting-container' }); - - // Function to update URL setting - const updateUrlSetting = () => { - // First, remove the existing URL setting if it exists - settingDiv.querySelectorAll('.url-setting').forEach(el => el.remove()); - - let urlSetting = new Setting(urlSettingDiv) - .setName(this.calendarType === 'vdir' ? 'Directory' : 'Calendar URL'); - urlSetting.settingEl.addClass('url-setting'); - - if (this.calendarType === 'vdir') { - // If vdir, add a dropdown - urlSetting.addDropdown(dropdown => { - const directories = this.listIcsDirectories(); - directories.forEach(dir => { - dropdown.addOption(dir, dir); - }); - dropdown.setValue(this.icsUrl).onChange(value => { - this.icsUrl = value; - }); - }); - } else { - // If remote, add a text input - urlSetting.addText(text => { - text.setValue(this.icsUrl).onChange(value => { - this.icsUrl = value; - }); - }); - } - }; - - // Call updateUrlSetting initially - updateUrlSetting(); - - const formatSetting = new Setting(settingDiv) - .setHeading().setName("Output Format"); - - // set each of the calendar format settings to the default if it's undefined - for (let f in DEFAULT_CALENDAR_FORMAT) { - if (this.format[f] == undefined) { - this.format[f] = DEFAULT_CALENDAR_FORMAT[f]; - } - } - - const checkboxToggle = new Setting(settingDiv) - .setName('Checkbox') - .setDesc('Use a checkbox for each event (will be a bullet otherwise)') - .addToggle(toggle => toggle - .setValue(this.format.checkbox || false) - .onChange(value => this.format.checkbox = value)); - - const endTimeToggle = new Setting(settingDiv) - .setName('End time') - .setDesc('Include the event\'s end time') - .addToggle(toggle => toggle - .setValue(this.format.includeEventEndTime || false) - .onChange(value => this.format.includeEventEndTime = value)); - - const icsNameToggle = new Setting(settingDiv) - .setName('Calendar name') - .setDesc('Include the calendar name') - .addToggle(toggle => toggle - .setValue(this.format.icsName || false) - .onChange(value => this.format.icsName = value)); - - const summaryToggle = new Setting(settingDiv) - .setName('Summary') - .setDesc('Include the summary field') - .addToggle(toggle => toggle - .setValue(this.format.summary || false) - .onChange(value => { - this.format.summary = value; - })); - - const locationToggle = new Setting(settingDiv) - .setName('Location') - .setDesc('Include the location field') - .addToggle(toggle => toggle - .setValue(this.format.location || false) - .onChange(value => { - this.format.location = value; - })); - - const descriptionToggle = new Setting(settingDiv) - .setName('Description') - .setDesc('Include the description field ') - .addToggle(toggle => toggle - .setValue(this.format.description || false) - .onChange(value => this.format.description = value)); - - let footerEl = contentEl.createDiv(); - let footerButtons = new Setting(footerEl); - footerButtons.addButton((b) => { - b.setTooltip("Save") - .setIcon("checkmark") - .onClick(async () => { - this.saved = true; - await this.plugin.saveSettings(); - this.close(); - }); - return b; - }); - footerButtons.addExtraButton((b) => { - b.setTooltip("Cancel") - .setIcon("cross") - .onClick(() => { - this.saved = false; - this.close(); - }); - return b; - }); - } - onOpen() { - this.display(); - } - - static setValidationError(textInput: TextComponent, message?: string) { - textInput.inputEl.addClass("is-invalid"); - if (message) { - textInput.inputEl.parentElement.addClasses([ - "has-invalid-message", - "unset-align-items" - ]); - textInput.inputEl.parentElement.parentElement.addClass( - ".unset-align-items" - ); - let mDiv = textInput.inputEl.parentElement.querySelector( - ".invalid-feedback" - ) as HTMLDivElement; - - if (!mDiv) { - mDiv = createDiv({ - cls: "invalid-feedback" - }); - } - mDiv.innerText = message; - mDiv.insertAfter(textInput.inputEl, null); - } - } - static removeValidationError(textInput: TextComponent) { - textInput.inputEl.removeClass("is-invalid"); - textInput.inputEl.parentElement.removeClasses([ - "has-invalid-message", - "unset-align-items" - ]); - textInput.inputEl.parentElement.parentElement.removeClass( - ".unset-align-items" - ); - - if (textInput.inputEl.parentElement.children[1]) { - textInput.inputEl.parentElement.removeChild( - textInput.inputEl.parentElement.children[1] - ); - } - } + plugin: ICSPlugin; + icsName: string = ""; + icsUrl: string = ""; + urlSetting: Setting; + urlText: TextComponent; + urlDropdown: DropdownComponent; + + saved: boolean = false; + error: boolean = false; + format: { + checkbox: boolean, + includeEventEndTime: boolean, + icsName: boolean, + summary: boolean, + location: boolean, + description: boolean, + showAttendees: boolean + } = DEFAULT_CALENDAR_FORMAT; + calendarType: string; + constructor(app: App, plugin: ICSPlugin, setting?: Calendar) { + super(app); + this.plugin = plugin; + if (setting) { + this.icsName = setting.icsName; + this.icsUrl = setting.icsUrl; + this.format = setting.format || this.format // if format is undefined, use default + this.calendarType = setting.calendarType || 'remote'; + } + } + + + listIcsDirectories(): string[] { + const icsFiles = this.app.vault.getFiles().filter(f => f.extension === "ics"); + const directories = new Set(icsFiles.map(f => f.parent.path)); + return Array.from(directories); + } + + display() { + let { + contentEl + } = this; + + contentEl.empty(); + + const settingDiv = contentEl.createDiv(); + + let nameText: TextComponent; + const nameSetting = new Setting(settingDiv) + .setName("Calendar Name") + .addText((text) => { + nameText = text; + nameText.setValue(this.icsName).onChange(async (v) => { + this.icsName = v; + }); + }); + + const calendarTypeSetting = new Setting(settingDiv) + .setName('Calendar Type') + .setDesc('Select the type of calendar (Remote URL or vdir)') + .addDropdown(dropdown => { + dropdown.addOption('remote', 'Remote URL'); + dropdown.addOption('vdir', 'vdir'); + dropdown.setValue(this.calendarType) + .onChange(value => { + this.calendarType = value as 'remote' | 'vdir'; + updateUrlSetting(); + }); + }); + + const urlSettingDiv = settingDiv.createDiv({ cls: 'url-setting-container' }); + + // Function to update URL setting + const updateUrlSetting = () => { + // First, remove the existing URL setting if it exists + settingDiv.querySelectorAll('.url-setting').forEach(el => el.remove()); + + let urlSetting = new Setting(urlSettingDiv) + .setName(this.calendarType === 'vdir' ? 'Directory' : 'Calendar URL'); + urlSetting.settingEl.addClass('url-setting'); + + if (this.calendarType === 'vdir') { + // If vdir, add a dropdown + urlSetting.addDropdown(dropdown => { + const directories = this.listIcsDirectories(); + directories.forEach(dir => { + dropdown.addOption(dir, dir); + }); + dropdown.setValue(this.icsUrl).onChange(value => { + this.icsUrl = value; + }); + }); + } else { + // If remote, add a text input + urlSetting.addText(text => { + text.setValue(this.icsUrl).onChange(value => { + this.icsUrl = value; + }); + }); + } + }; + + // Call updateUrlSetting initially + updateUrlSetting(); + + const formatSetting = new Setting(settingDiv) + .setHeading().setName("Output Format"); + + // set each of the calendar format settings to the default if it's undefined + for (let f in DEFAULT_CALENDAR_FORMAT) { + if (this.format[f] == undefined) { + this.format[f] = DEFAULT_CALENDAR_FORMAT[f]; + } + } + + const checkboxToggle = new Setting(settingDiv) + .setName('Checkbox') + .setDesc('Use a checkbox for each event (will be a bullet otherwise)') + .addToggle(toggle => toggle + .setValue(this.format.checkbox || false) + .onChange(value => this.format.checkbox = value)); + + const endTimeToggle = new Setting(settingDiv) + .setName('End time') + .setDesc('Include the event\'s end time') + .addToggle(toggle => toggle + .setValue(this.format.includeEventEndTime || false) + .onChange(value => this.format.includeEventEndTime = value)); + + const icsNameToggle = new Setting(settingDiv) + .setName('Calendar name') + .setDesc('Include the calendar name') + .addToggle(toggle => toggle + .setValue(this.format.icsName || false) + .onChange(value => this.format.icsName = value)); + + const summaryToggle = new Setting(settingDiv) + .setName('Summary') + .setDesc('Include the summary field') + .addToggle(toggle => toggle + .setValue(this.format.summary || false) + .onChange(value => { + this.format.summary = value; + })); + + const locationToggle = new Setting(settingDiv) + .setName('Location') + .setDesc('Include the location field') + .addToggle(toggle => toggle + .setValue(this.format.location || false) + .onChange(value => { + this.format.location = value; + })); + + const descriptionToggle = new Setting(settingDiv) + .setName('Description') + .setDesc('Include the description field ') + .addToggle(toggle => toggle + .setValue(this.format.description || false) + .onChange(value => this.format.description = value)); + + const showAttendeesToggle = new Setting(settingDiv) + .setName('Show Attendees') + .setDesc('Display attendees for the event') + .addToggle(toggle => toggle + .setValue(this.format.showAttendees || false) // Use the new property + .onChange(value => { + this.format.showAttendees = value; // Set the new property + })); + + let footerEl = contentEl.createDiv(); + let footerButtons = new Setting(footerEl); + footerButtons.addButton((b) => { + b.setTooltip("Save") + .setIcon("checkmark") + .onClick(async () => { + this.saved = true; + await this.plugin.saveSettings(); + this.close(); + }); + return b; + }); + footerButtons.addExtraButton((b) => { + b.setTooltip("Cancel") + .setIcon("cross") + .onClick(() => { + this.saved = false; + this.close(); + }); + return b; + }); + } + onOpen() { + this.display(); + } + + static setValidationError(textInput: TextComponent, message?: string) { + textInput.inputEl.addClass("is-invalid"); + if (message) { + textInput.inputEl.parentElement.addClasses([ + "has-invalid-message", + "unset-align-items" + ]); + textInput.inputEl.parentElement.parentElement.addClass( + ".unset-align-items" + ); + let mDiv = textInput.inputEl.parentElement.querySelector( + ".invalid-feedback" + ) as HTMLDivElement; + + if (!mDiv) { + mDiv = createDiv({ + cls: "invalid-feedback" + }); + } + mDiv.innerText = message; + mDiv.insertAfter(textInput.inputEl, null); + } + } + static removeValidationError(textInput: TextComponent) { + textInput.inputEl.removeClass("is-invalid"); + textInput.inputEl.parentElement.removeClasses([ + "has-invalid-message", + "unset-align-items" + ]); + textInput.inputEl.parentElement.parentElement.removeClass( + ".unset-align-items" + ); + + if (textInput.inputEl.parentElement.children[1]) { + textInput.inputEl.parentElement.removeChild( + textInput.inputEl.parentElement.children[1] + ); + } + } }