mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(ics): enhance ICS calendar integration
- Extend IcsManager with improved event parsing and caching - Add new ICS source configuration options - Improve IcsSettingsTab UI with better source management - Update ICS types with additional metadata fields - Remove unused styles from ics-settings.scss
This commit is contained in:
parent
7e05356927
commit
c456c38201
5 changed files with 824 additions and 185 deletions
|
|
@ -20,6 +20,11 @@ import {
|
|||
IcsTextReplacement,
|
||||
IcsHolidayConfig,
|
||||
} from "@/types/ics";
|
||||
import {
|
||||
isUrlIcsSource,
|
||||
LegacyIcsSource,
|
||||
AnyCalendarSource,
|
||||
} from "@/types/calendar-provider";
|
||||
import { t } from "@/translations/helper";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
import "@/styles/ics-settings.scss";
|
||||
|
|
@ -35,7 +40,7 @@ export class IcsSettingsComponent {
|
|||
constructor(
|
||||
plugin: TaskProgressBarPlugin,
|
||||
containerEl: HTMLElement,
|
||||
onBack?: () => void
|
||||
onBack?: () => void,
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.containerEl = containerEl;
|
||||
|
|
@ -47,47 +52,6 @@ export class IcsSettingsComponent {
|
|||
this.containerEl.empty();
|
||||
this.containerEl.addClass("ics-settings-container");
|
||||
|
||||
const backheader = this.containerEl.createDiv(
|
||||
"settings-tab-section-header"
|
||||
);
|
||||
// Header with back button
|
||||
const headerContainer = this.containerEl.createDiv(
|
||||
"ics-header-container"
|
||||
);
|
||||
|
||||
if (this.onBack) {
|
||||
const button = new ButtonComponent(backheader)
|
||||
.setClass("header-button")
|
||||
.onClick(() => {
|
||||
this.onBack?.();
|
||||
});
|
||||
|
||||
button.buttonEl.createEl(
|
||||
"span",
|
||||
{
|
||||
cls: "header-button-icon",
|
||||
},
|
||||
(el) => {
|
||||
setIcon(el, "arrow-left");
|
||||
}
|
||||
);
|
||||
button.buttonEl.createEl("span", {
|
||||
cls: "header-button-text",
|
||||
text: t("Back to main settings"),
|
||||
});
|
||||
}
|
||||
|
||||
headerContainer.createEl("h2", {
|
||||
text: t("ICS Calendar Integration"),
|
||||
});
|
||||
|
||||
headerContainer.createEl("p", {
|
||||
text: t(
|
||||
"Configure external calendar sources to display events in your task views."
|
||||
),
|
||||
cls: "ics-description",
|
||||
});
|
||||
|
||||
// Global settings
|
||||
this.displayGlobalSettings();
|
||||
|
||||
|
|
@ -96,7 +60,7 @@ export class IcsSettingsComponent {
|
|||
|
||||
// Add source button in a styled container
|
||||
const addSourceContainer = this.containerEl.createDiv(
|
||||
"ics-add-source-container"
|
||||
"ics-add-source-container",
|
||||
);
|
||||
const addButton = addSourceContainer.createEl("button", {
|
||||
text: "+ " + t("Add New Calendar Source"),
|
||||
|
|
@ -111,7 +75,7 @@ export class IcsSettingsComponent {
|
|||
|
||||
private displayGlobalSettings(): void {
|
||||
const globalContainer = this.containerEl.createDiv(
|
||||
"ics-global-settings"
|
||||
"ics-global-settings",
|
||||
);
|
||||
globalContainer.createEl("h3", { text: t("Global Settings") });
|
||||
|
||||
|
|
@ -119,7 +83,7 @@ export class IcsSettingsComponent {
|
|||
new Setting(globalContainer)
|
||||
.setName(t("Enable Background Refresh"))
|
||||
.setDesc(
|
||||
t("Automatically refresh calendar sources in the background")
|
||||
t("Automatically refresh calendar sources in the background"),
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
|
|
@ -208,21 +172,45 @@ export class IcsSettingsComponent {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a source is a URL-based ICS source (legacy or new format)
|
||||
*/
|
||||
private isUrlBasedSource(
|
||||
source: AnyCalendarSource,
|
||||
): source is IcsSource | LegacyIcsSource {
|
||||
// Legacy sources always have url, new sources need type check
|
||||
return "url" in source && typeof source.url === "string";
|
||||
}
|
||||
|
||||
private displaySourcesList(): void {
|
||||
const sourcesContainer = this.containerEl.createDiv("ics-sources-list");
|
||||
sourcesContainer.createEl("h3", { text: t("Calendar Sources") });
|
||||
|
||||
if (this.config.sources.length === 0) {
|
||||
// Filter to only show URL-based ICS sources (legacy tab)
|
||||
const urlSources = this.config.sources.filter((s) =>
|
||||
this.isUrlBasedSource(s),
|
||||
);
|
||||
|
||||
if (urlSources.length === 0) {
|
||||
const emptyState = sourcesContainer.createDiv("ics-empty-state");
|
||||
emptyState.createEl("p", {
|
||||
text: t(
|
||||
"No calendar sources configured. Add a source to get started."
|
||||
"No calendar sources configured. Add a source to get started.",
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.sources.forEach((source, index) => {
|
||||
urlSources.forEach((source) => {
|
||||
// Find the original index in config.sources for updates
|
||||
const index = this.config.sources.findIndex(
|
||||
(s) => s.id === source.id,
|
||||
);
|
||||
if (index === -1) return;
|
||||
|
||||
// Type assertion since we filtered for URL sources
|
||||
const urlSource = source as IcsSource;
|
||||
|
||||
const sourceContainer =
|
||||
sourcesContainer.createDiv("ics-source-item");
|
||||
|
||||
|
|
@ -230,37 +218,37 @@ export class IcsSettingsComponent {
|
|||
const sourceHeader = sourceContainer.createDiv("ics-source-header");
|
||||
|
||||
const titleContainer = sourceHeader.createDiv("ics-source-title");
|
||||
titleContainer.createEl("strong", { text: source.name });
|
||||
titleContainer.createEl("strong", { text: urlSource.name });
|
||||
|
||||
const statusEl = sourceHeader.createEl("span", {
|
||||
cls: "ics-source-status",
|
||||
});
|
||||
statusEl.setText(
|
||||
source.enabled ? t("ICS Enabled") : t("ICS Disabled")
|
||||
urlSource.enabled ? t("ICS Enabled") : t("ICS Disabled"),
|
||||
);
|
||||
statusEl.addClass(
|
||||
source.enabled ? "status-enabled" : "status-disabled"
|
||||
urlSource.enabled ? "status-enabled" : "status-disabled",
|
||||
);
|
||||
|
||||
// Source details
|
||||
const sourceDetails =
|
||||
sourceContainer.createDiv("ics-source-details");
|
||||
sourceDetails.createEl("div", {
|
||||
text: `${t("URL")}: ${this.truncateUrl(source.url)}`,
|
||||
title: source.url, // Show full URL on hover
|
||||
text: `${t("URL")}: ${this.truncateUrl(urlSource.url)}`,
|
||||
title: urlSource.url, // Show full URL on hover
|
||||
});
|
||||
sourceDetails.createEl("div", {
|
||||
text: `${t("Refresh")}: ${source.refreshInterval}${t("min")}`,
|
||||
text: `${t("Refresh")}: ${urlSource.refreshInterval}${t("min")}`,
|
||||
});
|
||||
if (source.color) {
|
||||
if (urlSource.color) {
|
||||
const colorDiv = sourceDetails.createEl("div");
|
||||
colorDiv.createSpan({ text: `${t("Color")}: ` });
|
||||
colorDiv.createEl("span", {
|
||||
attr: {
|
||||
style: `display: inline-block; width: 12px; height: 12px; background: ${source.color}; border-radius: 2px; margin-left: 4px; vertical-align: middle;`,
|
||||
style: `display: inline-block; width: 12px; height: 12px; background: ${urlSource.color}; border-radius: 2px; margin-left: 4px; vertical-align: middle;`,
|
||||
},
|
||||
});
|
||||
colorDiv.createSpan({ text: ` ${source.color}` });
|
||||
colorDiv.createSpan({ text: ` ${urlSource.color}` });
|
||||
}
|
||||
|
||||
// Source actions - reorganized for better UX
|
||||
|
|
@ -283,7 +271,7 @@ export class IcsSettingsComponent {
|
|||
this.config.sources[index] = updatedSource;
|
||||
this.saveAndRefresh();
|
||||
},
|
||||
source
|
||||
urlSource,
|
||||
).open();
|
||||
};
|
||||
|
||||
|
|
@ -302,14 +290,16 @@ export class IcsSettingsComponent {
|
|||
try {
|
||||
const icsManager = this.plugin.getIcsManager();
|
||||
if (icsManager) {
|
||||
const result = await icsManager.syncSource(source.id);
|
||||
const result = await icsManager.syncSource(
|
||||
urlSource.id,
|
||||
);
|
||||
if (result.success) {
|
||||
new Notice(t("Sync completed successfully"));
|
||||
syncButton.removeClass("syncing");
|
||||
syncButton.addClass("success");
|
||||
setTimeout(
|
||||
() => syncButton.removeClass("success"),
|
||||
2000
|
||||
2000,
|
||||
);
|
||||
} else {
|
||||
new Notice(t("Sync failed: ") + result.error);
|
||||
|
|
@ -317,7 +307,7 @@ export class IcsSettingsComponent {
|
|||
syncButton.addClass("error");
|
||||
setTimeout(
|
||||
() => syncButton.removeClass("error"),
|
||||
2000
|
||||
2000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -338,8 +328,8 @@ export class IcsSettingsComponent {
|
|||
|
||||
// Toggle button
|
||||
const toggleButton = secondaryActions.createEl("button", {
|
||||
text: source.enabled ? t("Disable") : t("Enable"),
|
||||
title: source.enabled
|
||||
text: urlSource.enabled ? t("Disable") : t("Enable"),
|
||||
title: urlSource.enabled
|
||||
? t("Disable this source")
|
||||
: t("Enable this source"),
|
||||
});
|
||||
|
|
@ -359,8 +349,8 @@ export class IcsSettingsComponent {
|
|||
if (
|
||||
confirm(
|
||||
t(
|
||||
"Are you sure you want to delete this calendar source?"
|
||||
)
|
||||
"Are you sure you want to delete this calendar source?",
|
||||
),
|
||||
)
|
||||
) {
|
||||
this.config.sources.splice(index, 1);
|
||||
|
|
@ -403,7 +393,7 @@ class IcsSourceModal extends Modal {
|
|||
constructor(
|
||||
app: App,
|
||||
onSave: (source: IcsSource) => void,
|
||||
existingSource?: IcsSource
|
||||
existingSource?: IcsSource,
|
||||
) {
|
||||
super(app);
|
||||
this.onSave = onSave;
|
||||
|
|
@ -452,12 +442,12 @@ class IcsSourceModal extends Modal {
|
|||
.setName(t("ICS URL"))
|
||||
.setDesc(
|
||||
t(
|
||||
"URL to the ICS/iCal file (supports http://, https://, and webcal:// protocols)"
|
||||
)
|
||||
"URL to the ICS/iCal file (supports http://, https://, and webcal:// protocols)",
|
||||
),
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder(
|
||||
"https://example.com/calendar.ics or webcal://example.com/calendar.ics"
|
||||
"https://example.com/calendar.ics or webcal://example.com/calendar.ics",
|
||||
)
|
||||
.setValue(this.source.url)
|
||||
.onChange((value) => {
|
||||
|
|
@ -470,16 +460,16 @@ class IcsSourceModal extends Modal {
|
|||
if (conversionResult.success) {
|
||||
const description =
|
||||
WebcalUrlConverter.getConversionDescription(
|
||||
conversionResult
|
||||
conversionResult,
|
||||
);
|
||||
// Find the description element and update it
|
||||
const descEl =
|
||||
text.inputEl.parentElement?.querySelector(
|
||||
".setting-item-description"
|
||||
".setting-item-description",
|
||||
);
|
||||
if (descEl) {
|
||||
descEl.textContent = `${t(
|
||||
"URL to the ICS/iCal file (supports http://, https://, and webcal:// protocols)"
|
||||
"URL to the ICS/iCal file (supports http://, https://, and webcal:// protocols)",
|
||||
)} - ${description}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -487,11 +477,11 @@ class IcsSourceModal extends Modal {
|
|||
// Reset description for non-webcal URLs
|
||||
const descEl =
|
||||
text.inputEl.parentElement?.querySelector(
|
||||
".setting-item-description"
|
||||
".setting-item-description",
|
||||
);
|
||||
if (descEl) {
|
||||
descEl.textContent = t(
|
||||
"URL to the ICS/iCal file (supports http://, https://, and webcal:// protocols)"
|
||||
"URL to the ICS/iCal file (supports http://, https://, and webcal:// protocols)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -541,7 +531,7 @@ class IcsSourceModal extends Modal {
|
|||
new Setting(contentEl)
|
||||
.setName(t("Show Type"))
|
||||
.setDesc(
|
||||
t("How to display events from this source in calendar views")
|
||||
t("How to display events from this source in calendar views"),
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
|
|
@ -645,7 +635,7 @@ class IcsSourceModal extends Modal {
|
|||
});
|
||||
textReplacementsContainer.createEl("p", {
|
||||
text: t(
|
||||
"Configure rules to modify event text using regular expressions"
|
||||
"Configure rules to modify event text using regular expressions",
|
||||
),
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
|
@ -657,7 +647,7 @@ class IcsSourceModal extends Modal {
|
|||
|
||||
// Container for replacement rules
|
||||
const rulesContainer = textReplacementsContainer.createDiv(
|
||||
"text-replacements-list"
|
||||
"text-replacements-list",
|
||||
);
|
||||
|
||||
const refreshRulesList = () => {
|
||||
|
|
@ -665,7 +655,7 @@ class IcsSourceModal extends Modal {
|
|||
|
||||
if (this.source.textReplacements!.length === 0) {
|
||||
const emptyState = rulesContainer.createDiv(
|
||||
"text-replacements-empty"
|
||||
"text-replacements-empty",
|
||||
);
|
||||
emptyState.createEl("p", {
|
||||
text: t("No text replacement rules configured"),
|
||||
|
|
@ -674,12 +664,12 @@ class IcsSourceModal extends Modal {
|
|||
} else {
|
||||
this.source.textReplacements!.forEach((rule, index) => {
|
||||
const ruleContainer = rulesContainer.createDiv(
|
||||
"text-replacement-rule"
|
||||
"text-replacement-rule",
|
||||
);
|
||||
|
||||
// Rule header
|
||||
const ruleHeader = ruleContainer.createDiv(
|
||||
"text-replacement-header"
|
||||
"text-replacement-header",
|
||||
);
|
||||
const titleEl = ruleHeader.createEl("strong", {
|
||||
text: rule.name || `Rule ${index + 1}`,
|
||||
|
|
@ -694,7 +684,7 @@ class IcsSourceModal extends Modal {
|
|||
|
||||
// Rule details
|
||||
const ruleDetails = ruleContainer.createDiv(
|
||||
"text-replacement-details"
|
||||
"text-replacement-details",
|
||||
);
|
||||
ruleDetails.createEl("div", {
|
||||
text: `${t("Target")}: ${rule.target}`,
|
||||
|
|
@ -710,7 +700,7 @@ class IcsSourceModal extends Modal {
|
|||
|
||||
// Rule actions
|
||||
const ruleActions = ruleContainer.createDiv(
|
||||
"text-replacement-actions"
|
||||
"text-replacement-actions",
|
||||
);
|
||||
|
||||
const editButton = ruleActions.createEl("button", {
|
||||
|
|
@ -725,7 +715,7 @@ class IcsSourceModal extends Modal {
|
|||
updatedRule;
|
||||
refreshRulesList();
|
||||
},
|
||||
rule
|
||||
rule,
|
||||
).open();
|
||||
};
|
||||
|
||||
|
|
@ -746,8 +736,8 @@ class IcsSourceModal extends Modal {
|
|||
if (
|
||||
confirm(
|
||||
t(
|
||||
"Are you sure you want to delete this text replacement rule?"
|
||||
)
|
||||
"Are you sure you want to delete this text replacement rule?",
|
||||
),
|
||||
)
|
||||
) {
|
||||
this.source.textReplacements!.splice(index, 1);
|
||||
|
|
@ -762,7 +752,7 @@ class IcsSourceModal extends Modal {
|
|||
|
||||
// Add rule button
|
||||
const addRuleContainer = textReplacementsContainer.createDiv(
|
||||
"text-replacement-add"
|
||||
"text-replacement-add",
|
||||
);
|
||||
const addButton = addRuleContainer.createEl("button", {
|
||||
text: "+ " + t("Add Text Replacement Rule"),
|
||||
|
|
@ -791,7 +781,7 @@ class IcsSourceModal extends Modal {
|
|||
.setClass("auth-field")
|
||||
.addText((text) => {
|
||||
text.setValue(
|
||||
this.source.auth?.username || ""
|
||||
this.source.auth?.username || "",
|
||||
).onChange((value) => {
|
||||
if (this.source.auth) {
|
||||
this.source.auth.username = value;
|
||||
|
|
@ -804,7 +794,7 @@ class IcsSourceModal extends Modal {
|
|||
.setClass("auth-field")
|
||||
.addText((text) => {
|
||||
text.setValue(
|
||||
this.source.auth?.password || ""
|
||||
this.source.auth?.password || "",
|
||||
).onChange((value) => {
|
||||
if (this.source.auth) {
|
||||
this.source.auth.password = value;
|
||||
|
|
@ -824,7 +814,7 @@ class IcsSourceModal extends Modal {
|
|||
if (this.source.auth) {
|
||||
this.source.auth.token = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
|
@ -839,8 +829,8 @@ class IcsSourceModal extends Modal {
|
|||
JSON.stringify(
|
||||
this.source.auth?.headers || {},
|
||||
null,
|
||||
2
|
||||
)
|
||||
2,
|
||||
),
|
||||
).onChange((value) => {
|
||||
try {
|
||||
const headers = JSON.parse(value);
|
||||
|
|
@ -910,7 +900,7 @@ class IcsSourceModal extends Modal {
|
|||
new Setting(statusContainer)
|
||||
.setName(t("Enable Status Mapping"))
|
||||
.setDesc(
|
||||
t("Automatically map ICS events to specific task statuses")
|
||||
t("Automatically map ICS events to specific task statuses"),
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
|
|
@ -955,7 +945,7 @@ class IcsSourceModal extends Modal {
|
|||
new Setting(container)
|
||||
.setName(t("Maximum Gap Days"))
|
||||
.setDesc(
|
||||
t("Maximum days between events to consider them consecutive")
|
||||
t("Maximum days between events to consider them consecutive"),
|
||||
)
|
||||
.setClass("holiday-setting")
|
||||
.addText((text) => {
|
||||
|
|
@ -1003,14 +993,14 @@ class IcsSourceModal extends Modal {
|
|||
new Setting(patternsContainer)
|
||||
.setName(t("Summary Patterns"))
|
||||
.setDesc(
|
||||
t("Regex patterns to match in event titles (one per line)")
|
||||
t("Regex patterns to match in event titles (one per line)"),
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setValue(
|
||||
(
|
||||
this.source.holidayConfig!.detectionPatterns.summary ||
|
||||
[]
|
||||
).join("\n")
|
||||
).join("\n"),
|
||||
).onChange((value) => {
|
||||
this.source.holidayConfig!.detectionPatterns.summary = value
|
||||
.split("\n")
|
||||
|
|
@ -1028,7 +1018,7 @@ class IcsSourceModal extends Modal {
|
|||
(
|
||||
this.source.holidayConfig!.detectionPatterns.keywords ||
|
||||
[]
|
||||
).join("\n")
|
||||
).join("\n"),
|
||||
).onChange((value) => {
|
||||
this.source.holidayConfig!.detectionPatterns.keywords =
|
||||
value
|
||||
|
|
@ -1042,14 +1032,14 @@ class IcsSourceModal extends Modal {
|
|||
new Setting(patternsContainer)
|
||||
.setName(t("Categories"))
|
||||
.setDesc(
|
||||
t("Event categories that indicate holidays (one per line)")
|
||||
t("Event categories that indicate holidays (one per line)"),
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setValue(
|
||||
(
|
||||
this.source.holidayConfig!.detectionPatterns
|
||||
.categories || []
|
||||
).join("\n")
|
||||
).join("\n"),
|
||||
).onChange((value) => {
|
||||
this.source.holidayConfig!.detectionPatterns.categories =
|
||||
value
|
||||
|
|
@ -1064,14 +1054,14 @@ class IcsSourceModal extends Modal {
|
|||
.setName(t("Group Display Format"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Format for grouped holiday display. Use {title}, {count}, {startDate}, {endDate}"
|
||||
)
|
||||
"Format for grouped holiday display. Use {title}, {count}, {startDate}, {endDate}",
|
||||
),
|
||||
)
|
||||
.setClass("holiday-setting")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("{title} ({count} days)")
|
||||
.setValue(
|
||||
this.source.holidayConfig!.groupDisplayFormat || ""
|
||||
this.source.holidayConfig!.groupDisplayFormat || "",
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.source.holidayConfig!.groupDisplayFormat =
|
||||
|
|
@ -1083,7 +1073,7 @@ class IcsSourceModal extends Modal {
|
|||
private refreshStatusMappingSettings(container: HTMLElement): void {
|
||||
// Remove existing status mapping settings
|
||||
const existingSettings = container.querySelectorAll(
|
||||
".status-mapping-setting"
|
||||
".status-mapping-setting",
|
||||
);
|
||||
existingSettings.forEach((setting) => setting.remove());
|
||||
|
||||
|
|
@ -1138,7 +1128,7 @@ class IcsSourceModal extends Modal {
|
|||
.addOption("/", t("Status In Progress"))
|
||||
.addOption("?", t("Status Question"))
|
||||
.setValue(
|
||||
this.source.statusMapping!.timingRules.currentEvents
|
||||
this.source.statusMapping!.timingRules.currentEvents,
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.source.statusMapping!.timingRules.currentEvents =
|
||||
|
|
@ -1158,7 +1148,7 @@ class IcsSourceModal extends Modal {
|
|||
.addOption("/", t("Status In Progress"))
|
||||
.addOption("?", t("Status Question"))
|
||||
.setValue(
|
||||
this.source.statusMapping!.timingRules.futureEvents
|
||||
this.source.statusMapping!.timingRules.futureEvents,
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.source.statusMapping!.timingRules.futureEvents =
|
||||
|
|
@ -1171,7 +1161,7 @@ class IcsSourceModal extends Modal {
|
|||
propertyContainer.createEl("h4", { text: t("Property Rules") });
|
||||
propertyContainer.createEl("p", {
|
||||
text: t(
|
||||
"Optional rules based on event properties (higher priority than timing rules)"
|
||||
"Optional rules based on event properties (higher priority than timing rules)",
|
||||
),
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
|
@ -1195,7 +1185,7 @@ class IcsSourceModal extends Modal {
|
|||
.addOption("?", t("Status Question"))
|
||||
.setValue(
|
||||
this.source.statusMapping!.propertyRules!.holidayMapping
|
||||
?.holidayStatus || ""
|
||||
?.holidayStatus || "",
|
||||
)
|
||||
.onChange((value) => {
|
||||
if (
|
||||
|
|
@ -1222,8 +1212,8 @@ class IcsSourceModal extends Modal {
|
|||
.setName(t("Category Mapping"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Map specific categories to statuses (format: category:status, one per line)"
|
||||
)
|
||||
"Map specific categories to statuses (format: category:status, one per line)",
|
||||
),
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
const categoryMapping =
|
||||
|
|
@ -1272,11 +1262,11 @@ class IcsSourceModal extends Modal {
|
|||
|
||||
// Use WebcalUrlConverter for URL validation
|
||||
const conversionResult = WebcalUrlConverter.convertWebcalUrl(
|
||||
this.source.url
|
||||
this.source.url,
|
||||
);
|
||||
if (!conversionResult.success) {
|
||||
new Notice(
|
||||
t("Please enter a valid URL") + ": " + conversionResult.error
|
||||
t("Please enter a valid URL") + ": " + conversionResult.error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1300,7 +1290,7 @@ class TextReplacementModal extends Modal {
|
|||
constructor(
|
||||
app: App,
|
||||
onSave: (rule: IcsTextReplacement) => void,
|
||||
existingRule?: IcsTextReplacement
|
||||
existingRule?: IcsTextReplacement,
|
||||
) {
|
||||
super(app);
|
||||
this.onSave = onSave;
|
||||
|
|
@ -1385,11 +1375,11 @@ class TextReplacementModal extends Modal {
|
|||
if (this.rule.pattern && input) {
|
||||
const regex = new RegExp(
|
||||
this.rule.pattern,
|
||||
this.rule.flags || "g"
|
||||
this.rule.flags || "g",
|
||||
);
|
||||
const result = input.replace(regex, this.rule.replacement);
|
||||
const resultSpan = testOutput.querySelector(
|
||||
".test-result"
|
||||
".test-result",
|
||||
) as HTMLElement;
|
||||
if (resultSpan) {
|
||||
resultSpan.textContent = result;
|
||||
|
|
@ -1398,7 +1388,7 @@ class TextReplacementModal extends Modal {
|
|||
}
|
||||
} else {
|
||||
const resultSpan = testOutput.querySelector(
|
||||
".test-result"
|
||||
".test-result",
|
||||
) as HTMLElement;
|
||||
if (resultSpan) {
|
||||
resultSpan.textContent = input || "";
|
||||
|
|
@ -1407,7 +1397,7 @@ class TextReplacementModal extends Modal {
|
|||
}
|
||||
} catch (error) {
|
||||
const resultSpan = testOutput.querySelector(
|
||||
".test-result"
|
||||
".test-result",
|
||||
) as HTMLElement;
|
||||
if (resultSpan) {
|
||||
resultSpan.textContent = "Invalid regex pattern";
|
||||
|
|
@ -1421,8 +1411,8 @@ class TextReplacementModal extends Modal {
|
|||
.setName(t("Pattern (Regular Expression)"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Regular expression pattern to match. Use parentheses for capture groups."
|
||||
)
|
||||
"Regular expression pattern to match. Use parentheses for capture groups.",
|
||||
),
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("^Meeting: ")
|
||||
|
|
@ -1440,8 +1430,8 @@ class TextReplacementModal extends Modal {
|
|||
.setName(t("Replacement"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Text to replace matches with. Use $1, $2, etc. for capture groups."
|
||||
)
|
||||
"Text to replace matches with. Use $1, $2, etc. for capture groups.",
|
||||
),
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
|
|
@ -1459,8 +1449,8 @@ class TextReplacementModal extends Modal {
|
|||
.setName(t("Regex Flags"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Regular expression flags (e.g., 'g' for global, 'i' for case-insensitive)"
|
||||
)
|
||||
"Regular expression flags (e.g., 'g' for global, 'i' for case-insensitive)",
|
||||
),
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("g")
|
||||
|
|
@ -1521,7 +1511,7 @@ class TextReplacementModal extends Modal {
|
|||
text.setPlaceholder("Meeting: Weekly Standup").onChange(
|
||||
(value) => {
|
||||
updateTestOutput(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export class IcsSource {
|
|||
*/
|
||||
private subscribeToIcsUpdates(): void {
|
||||
// Listen for ICS cache updates
|
||||
this.app.workspace.on("ics-cache-updated" as any, () => {
|
||||
this.app.workspace.on("task-genius:ics-cache-updated", () => {
|
||||
console.log("[IcsSource] ICS cache updated, reloading events...");
|
||||
this.loadAndEmitIcsEvents();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,21 @@ import {
|
|||
IcsTextReplacement,
|
||||
IcsEventWithHoliday,
|
||||
} from "../types/ics";
|
||||
import { Task, ExtendedMetadata, EnhancedStandardTaskMetadata } from "../types/task";
|
||||
import {
|
||||
AnyCalendarSource,
|
||||
LegacyIcsSource,
|
||||
UrlIcsSourceConfig,
|
||||
isUrlIcsSource,
|
||||
isGoogleSource,
|
||||
isOutlookSource,
|
||||
isAppleSource,
|
||||
CalendarSource,
|
||||
} from "../types/calendar-provider";
|
||||
import {
|
||||
Task,
|
||||
ExtendedMetadata,
|
||||
EnhancedStandardTaskMetadata,
|
||||
} from "../types/task";
|
||||
import { IcsParser } from "../parsers/ics-parser";
|
||||
import { HolidayDetector } from "../parsers/holiday-detector";
|
||||
import { StatusMapper } from "../parsers/ics-status-mapper";
|
||||
|
|
@ -24,6 +38,12 @@ import { TaskProgressBarSettings } from "../common/setting-definition";
|
|||
import { TimeParsingService } from "../services/time-parsing-service";
|
||||
import { TimeComponent } from "../types/time-parsing";
|
||||
import TaskProgressBarPlugin from "src";
|
||||
import { CalendarAuthManager } from "./calendar-auth-manager";
|
||||
import {
|
||||
CalendarSourceManager,
|
||||
WriteResult,
|
||||
UpdateEventOptions,
|
||||
} from "../providers/index";
|
||||
|
||||
export class IcsManager extends Component {
|
||||
private config: IcsManagerConfig;
|
||||
|
|
@ -33,6 +53,7 @@ export class IcsManager extends Component {
|
|||
private onEventsUpdated?: (sourceId: string, events: IcsEvent[]) => void;
|
||||
private pluginSettings: TaskProgressBarSettings;
|
||||
private timeParsingService?: TimeParsingService;
|
||||
private calendarSourceManager?: CalendarSourceManager;
|
||||
|
||||
private plugin?: TaskProgressBarPlugin;
|
||||
|
||||
|
|
@ -41,12 +62,43 @@ export class IcsManager extends Component {
|
|||
pluginSettings: TaskProgressBarSettings,
|
||||
plugin?: TaskProgressBarPlugin,
|
||||
timeParsingService?: TimeParsingService,
|
||||
authManager?: CalendarAuthManager,
|
||||
) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.pluginSettings = pluginSettings;
|
||||
this.plugin = plugin;
|
||||
this.timeParsingService = timeParsingService;
|
||||
|
||||
// Initialize CalendarSourceManager for OAuth providers
|
||||
if (authManager) {
|
||||
this.calendarSourceManager = new CalendarSourceManager(authManager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a source is a URL-based ICS source (legacy or new format)
|
||||
* These are the only sources this manager can fetch directly
|
||||
*/
|
||||
private isUrlBasedSource(
|
||||
source: AnyCalendarSource,
|
||||
): source is IcsSource | LegacyIcsSource | UrlIcsSourceConfig {
|
||||
return "url" in source && typeof source.url === "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL-based sources from configuration
|
||||
*/
|
||||
private getUrlBasedSources(): (
|
||||
| IcsSource
|
||||
| LegacyIcsSource
|
||||
| UrlIcsSourceConfig
|
||||
)[] {
|
||||
return this.config.sources.filter((s) => this.isUrlBasedSource(s)) as (
|
||||
| IcsSource
|
||||
| LegacyIcsSource
|
||||
| UrlIcsSourceConfig
|
||||
)[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,6 +209,7 @@ export class IcsManager extends Component {
|
|||
console.log("source", source, "sourceId", sourceId);
|
||||
console.log("cacheEntry events count", cacheEntry.events.length);
|
||||
|
||||
// Process all enabled sources (URL-based and OAuth-based)
|
||||
if (source?.enabled) {
|
||||
console.log("Source is enabled, applying filters");
|
||||
// Apply filters if configured
|
||||
|
|
@ -201,6 +254,7 @@ export class IcsManager extends Component {
|
|||
);
|
||||
console.log("Cache entry events count:", cacheEntry.events.length);
|
||||
|
||||
// Process all enabled sources (URL-based and OAuth-based)
|
||||
if (source?.enabled) {
|
||||
// Apply filters first
|
||||
const filteredEvents = this.applyFilters(
|
||||
|
|
@ -210,13 +264,17 @@ export class IcsManager extends Component {
|
|||
|
||||
console.log("Filtered events count:", filteredEvents.length);
|
||||
|
||||
// Apply holiday detection if configured
|
||||
// Apply holiday detection if configured (only available on URL/legacy sources)
|
||||
let processedEvents: IcsEventWithHoliday[];
|
||||
if (source.holidayConfig?.enabled) {
|
||||
const holidayConfig =
|
||||
"holidayConfig" in source
|
||||
? source.holidayConfig
|
||||
: undefined;
|
||||
if (holidayConfig?.enabled) {
|
||||
processedEvents =
|
||||
HolidayDetector.processEventsWithHolidayDetection(
|
||||
filteredEvents,
|
||||
source.holidayConfig,
|
||||
holidayConfig,
|
||||
);
|
||||
} else {
|
||||
// Convert to IcsEventWithHoliday format without holiday detection
|
||||
|
|
@ -323,6 +381,7 @@ export class IcsManager extends Component {
|
|||
const cacheEntry = this.cache.get(sourceId);
|
||||
const source = this.config.sources.find((s) => s.id === sourceId);
|
||||
|
||||
// Handle all enabled sources
|
||||
if (!cacheEntry || !source?.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -387,7 +446,7 @@ export class IcsManager extends Component {
|
|||
project: event.source.name,
|
||||
context: processedEvent.location,
|
||||
heading: [],
|
||||
|
||||
|
||||
// Enhanced time components
|
||||
...enhancedMetadata,
|
||||
},
|
||||
|
|
@ -397,12 +456,15 @@ export class IcsManager extends Component {
|
|||
description: processedEvent.description,
|
||||
location: processedEvent.location,
|
||||
},
|
||||
readonly: true,
|
||||
// OAuth providers (Google, Outlook, Apple) support two-way sync; URL ICS is read-only
|
||||
readonly: !this.isOAuthSource(event.source as AnyCalendarSource),
|
||||
badge: event.source.showType === "badge",
|
||||
source: {
|
||||
type: "ics",
|
||||
name: event.source.name,
|
||||
id: event.source.id,
|
||||
providerType:
|
||||
(event.source as AnyCalendarSource).type || "url-ics",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -416,7 +478,7 @@ export class IcsManager extends Component {
|
|||
event: IcsEventWithHoliday,
|
||||
): Task<ExtendedMetadata> & {
|
||||
icsEvent: IcsEvent;
|
||||
readonly: true;
|
||||
readonly: boolean;
|
||||
badge: boolean;
|
||||
source: { type: "ics"; name: string; id: string };
|
||||
} {
|
||||
|
|
@ -466,7 +528,7 @@ export class IcsManager extends Component {
|
|||
project: event.source.name,
|
||||
context: processedEvent.location,
|
||||
heading: [],
|
||||
|
||||
|
||||
// Enhanced time components
|
||||
...enhancedMetadata,
|
||||
} as any, // Use any to allow additional holiday fields
|
||||
|
|
@ -476,12 +538,15 @@ export class IcsManager extends Component {
|
|||
description: processedEvent.description,
|
||||
location: processedEvent.location,
|
||||
},
|
||||
readonly: true,
|
||||
// OAuth providers (Google, Outlook, Apple) support two-way sync; URL ICS is read-only
|
||||
readonly: !this.isOAuthSource(event.source as AnyCalendarSource),
|
||||
badge: event.source.showType === "badge",
|
||||
source: {
|
||||
type: "ics",
|
||||
name: event.source.name,
|
||||
id: event.source.id,
|
||||
providerType:
|
||||
(event.source as AnyCalendarSource).type || "url-ics",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -493,7 +558,7 @@ export class IcsManager extends Component {
|
|||
*/
|
||||
private extractTimeComponentsFromIcsEvent(
|
||||
event: IcsEvent,
|
||||
processedEvent: IcsEvent
|
||||
processedEvent: IcsEvent,
|
||||
): Partial<EnhancedStandardTaskMetadata> {
|
||||
if (!this.timeParsingService) {
|
||||
return {};
|
||||
|
|
@ -501,11 +566,14 @@ export class IcsManager extends Component {
|
|||
|
||||
try {
|
||||
// Create time components from ICS event times
|
||||
const timeComponents: EnhancedStandardTaskMetadata["timeComponents"] = {};
|
||||
const timeComponents: EnhancedStandardTaskMetadata["timeComponents"] =
|
||||
{};
|
||||
|
||||
// Extract time from ICS dtstart (start time)
|
||||
if (event.dtstart && !event.allDay) {
|
||||
const startTimeComponent = this.createTimeComponentFromDate(event.dtstart);
|
||||
const startTimeComponent = this.createTimeComponentFromDate(
|
||||
event.dtstart,
|
||||
);
|
||||
if (startTimeComponent) {
|
||||
timeComponents.startTime = startTimeComponent;
|
||||
timeComponents.scheduledTime = startTimeComponent; // ICS events are typically scheduled
|
||||
|
|
@ -514,29 +582,39 @@ export class IcsManager extends Component {
|
|||
|
||||
// Extract time from ICS dtend (end time)
|
||||
if (event.dtend && !event.allDay) {
|
||||
const endTimeComponent = this.createTimeComponentFromDate(event.dtend);
|
||||
const endTimeComponent = this.createTimeComponentFromDate(
|
||||
event.dtend,
|
||||
);
|
||||
if (endTimeComponent) {
|
||||
timeComponents.endTime = endTimeComponent;
|
||||
timeComponents.dueTime = endTimeComponent; // End time can be considered due time
|
||||
|
||||
|
||||
// Create range relationship if both start and end exist
|
||||
if (timeComponents.startTime) {
|
||||
timeComponents.startTime.isRange = true;
|
||||
timeComponents.startTime.rangePartner = endTimeComponent;
|
||||
timeComponents.startTime.rangePartner =
|
||||
endTimeComponent;
|
||||
endTimeComponent.isRange = true;
|
||||
endTimeComponent.rangePartner = timeComponents.startTime;
|
||||
endTimeComponent.rangePartner =
|
||||
timeComponents.startTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also parse time components from event description and summary if available
|
||||
let descriptionTimeComponents: EnhancedStandardTaskMetadata["timeComponents"] = {};
|
||||
const textToParse = [processedEvent.summary, processedEvent.description, processedEvent.location]
|
||||
let descriptionTimeComponents: EnhancedStandardTaskMetadata["timeComponents"] =
|
||||
{};
|
||||
const textToParse = [
|
||||
processedEvent.summary,
|
||||
processedEvent.description,
|
||||
processedEvent.location,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
.join(" ");
|
||||
|
||||
if (textToParse.trim()) {
|
||||
const { timeComponents: parsedComponents } = this.timeParsingService.parseTimeComponents(textToParse);
|
||||
const { timeComponents: parsedComponents } =
|
||||
this.timeParsingService.parseTimeComponents(textToParse);
|
||||
descriptionTimeComponents = parsedComponents;
|
||||
}
|
||||
|
||||
|
|
@ -548,7 +626,10 @@ export class IcsManager extends Component {
|
|||
};
|
||||
|
||||
// Create enhanced datetime objects
|
||||
const enhancedDates = this.createEnhancedDateTimesFromIcs(event, mergedTimeComponents);
|
||||
const enhancedDates = this.createEnhancedDateTimesFromIcs(
|
||||
event,
|
||||
mergedTimeComponents,
|
||||
);
|
||||
|
||||
const enhancedMetadata: Partial<EnhancedStandardTaskMetadata> = {};
|
||||
|
||||
|
|
@ -562,7 +643,10 @@ export class IcsManager extends Component {
|
|||
|
||||
return enhancedMetadata;
|
||||
} catch (error) {
|
||||
console.error(`[IcsManager] Failed to extract time components from ICS event ${event.uid}:`, error);
|
||||
console.error(
|
||||
`[IcsManager] Failed to extract time components from ICS event ${event.uid}:`,
|
||||
error,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
@ -576,13 +660,13 @@ export class IcsManager extends Component {
|
|||
}
|
||||
|
||||
// Format time as HH:MM or HH:MM:SS depending on whether seconds are present
|
||||
const hours = date.getUTCHours().toString().padStart(2, '0');
|
||||
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
|
||||
const hours = date.getUTCHours().toString().padStart(2, "0");
|
||||
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
|
||||
const seconds = date.getUTCSeconds();
|
||||
|
||||
|
||||
let originalText = `${hours}:${minutes}`;
|
||||
if (seconds > 0) {
|
||||
originalText += `:${seconds.toString().padStart(2, '0')}`;
|
||||
originalText += `:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -599,7 +683,7 @@ export class IcsManager extends Component {
|
|||
*/
|
||||
private createEnhancedDateTimesFromIcs(
|
||||
event: IcsEvent,
|
||||
timeComponents: EnhancedStandardTaskMetadata["timeComponents"]
|
||||
timeComponents: EnhancedStandardTaskMetadata["timeComponents"],
|
||||
): EnhancedStandardTaskMetadata["enhancedDates"] {
|
||||
const enhancedDates: EnhancedStandardTaskMetadata["enhancedDates"] = {};
|
||||
|
||||
|
|
@ -618,41 +702,65 @@ export class IcsManager extends Component {
|
|||
// try to combine the date from ICS with the parsed time components
|
||||
if (event.allDay && timeComponents) {
|
||||
const eventDate = new Date(event.dtstart);
|
||||
|
||||
|
||||
if (timeComponents.startTime) {
|
||||
const startDateTime = new Date(eventDate);
|
||||
startDateTime.setHours(timeComponents.startTime.hour, timeComponents.startTime.minute, timeComponents.startTime.second || 0);
|
||||
startDateTime.setHours(
|
||||
timeComponents.startTime.hour,
|
||||
timeComponents.startTime.minute,
|
||||
timeComponents.startTime.second || 0,
|
||||
);
|
||||
enhancedDates.startDateTime = startDateTime;
|
||||
enhancedDates.scheduledDateTime = startDateTime;
|
||||
}
|
||||
|
||||
|
||||
if (timeComponents.endTime) {
|
||||
const endDateTime = new Date(eventDate);
|
||||
endDateTime.setHours(timeComponents.endTime.hour, timeComponents.endTime.minute, timeComponents.endTime.second || 0);
|
||||
|
||||
endDateTime.setHours(
|
||||
timeComponents.endTime.hour,
|
||||
timeComponents.endTime.minute,
|
||||
timeComponents.endTime.second || 0,
|
||||
);
|
||||
|
||||
// Handle midnight crossing for time ranges
|
||||
if (timeComponents.startTime && timeComponents.endTime.hour < timeComponents.startTime.hour) {
|
||||
if (
|
||||
timeComponents.startTime &&
|
||||
timeComponents.endTime.hour < timeComponents.startTime.hour
|
||||
) {
|
||||
endDateTime.setDate(endDateTime.getDate() + 1);
|
||||
}
|
||||
|
||||
|
||||
enhancedDates.endDateTime = endDateTime;
|
||||
enhancedDates.dueDateTime = endDateTime;
|
||||
}
|
||||
|
||||
|
||||
if (timeComponents.dueTime && !enhancedDates.dueDateTime) {
|
||||
const dueDateTime = new Date(eventDate);
|
||||
dueDateTime.setHours(timeComponents.dueTime.hour, timeComponents.dueTime.minute, timeComponents.dueTime.second || 0);
|
||||
dueDateTime.setHours(
|
||||
timeComponents.dueTime.hour,
|
||||
timeComponents.dueTime.minute,
|
||||
timeComponents.dueTime.second || 0,
|
||||
);
|
||||
enhancedDates.dueDateTime = dueDateTime;
|
||||
}
|
||||
|
||||
if (timeComponents.scheduledTime && !enhancedDates.scheduledDateTime) {
|
||||
|
||||
if (
|
||||
timeComponents.scheduledTime &&
|
||||
!enhancedDates.scheduledDateTime
|
||||
) {
|
||||
const scheduledDateTime = new Date(eventDate);
|
||||
scheduledDateTime.setHours(timeComponents.scheduledTime.hour, timeComponents.scheduledTime.minute, timeComponents.scheduledTime.second || 0);
|
||||
scheduledDateTime.setHours(
|
||||
timeComponents.scheduledTime.hour,
|
||||
timeComponents.scheduledTime.minute,
|
||||
timeComponents.scheduledTime.second || 0,
|
||||
);
|
||||
enhancedDates.scheduledDateTime = scheduledDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(enhancedDates).length > 0 ? enhancedDates : undefined;
|
||||
return Object.keys(enhancedDates).length > 0
|
||||
? enhancedDates
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -688,6 +796,17 @@ export class IcsManager extends Component {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a source is an OAuth-based source (Google, Outlook, Apple)
|
||||
*/
|
||||
private isOAuthSource(source: AnyCalendarSource): boolean {
|
||||
return (
|
||||
isGoogleSource(source as CalendarSource) ||
|
||||
isOutlookSource(source as CalendarSource) ||
|
||||
isAppleSource(source as CalendarSource)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually sync a specific source
|
||||
*/
|
||||
|
|
@ -697,6 +816,25 @@ export class IcsManager extends Component {
|
|||
throw new Error(`Source not found: ${sourceId}`);
|
||||
}
|
||||
|
||||
// Handle OAuth sources via CalendarSourceManager
|
||||
if (this.isOAuthSource(source)) {
|
||||
if (!this.calendarSourceManager) {
|
||||
return {
|
||||
success: false,
|
||||
error: "OAuth provider manager not initialized. Please configure the calendar authentication first.",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
return this.syncOAuthSource(source as CalendarSource);
|
||||
}
|
||||
|
||||
// Only URL-based sources can be synced by this manager directly
|
||||
if (!this.isUrlBasedSource(source)) {
|
||||
throw new Error(
|
||||
`Source ${sourceId} is not a supported calendar source type`,
|
||||
);
|
||||
}
|
||||
|
||||
this.updateSyncStatus(sourceId, { status: "syncing" });
|
||||
|
||||
try {
|
||||
|
|
@ -784,15 +922,136 @@ export class IcsManager extends Component {
|
|||
const syncPromises = this.config.sources
|
||||
.filter((source) => source.enabled)
|
||||
.map(async (source) => {
|
||||
const result = await this.syncSource(source.id);
|
||||
results.set(source.id, result);
|
||||
return result;
|
||||
try {
|
||||
const result = await this.syncSource(source.id);
|
||||
results.set(source.id, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error";
|
||||
console.warn(
|
||||
`Failed to sync source ${source.id}:`,
|
||||
errorMessage,
|
||||
);
|
||||
const failedResult: IcsFetchResult = {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
results.set(source.id, failedResult);
|
||||
return failedResult;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(syncPromises);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync an OAuth-based source (Google, Outlook, Apple CalDAV)
|
||||
*/
|
||||
private async syncOAuthSource(
|
||||
source: CalendarSource,
|
||||
): Promise<IcsFetchResult> {
|
||||
if (!this.calendarSourceManager) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Calendar Source Manager not initialized",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
this.updateSyncStatus(source.id, { status: "syncing" });
|
||||
|
||||
try {
|
||||
const provider = this.calendarSourceManager.getProvider(source);
|
||||
|
||||
// Calculate date range for sync: -30 days to +90 days (120 day window)
|
||||
const now = new Date();
|
||||
const start = new Date(now);
|
||||
start.setDate(now.getDate() - 30);
|
||||
const end = new Date(now);
|
||||
end.setDate(now.getDate() + 90);
|
||||
|
||||
console.log(
|
||||
`[IcsManager] Syncing OAuth source ${source.id} (${source.type})...`,
|
||||
);
|
||||
|
||||
const events = await provider.getEvents({
|
||||
range: { start, end },
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[IcsManager] Fetched ${events.length} events from OAuth source ${source.id}`,
|
||||
);
|
||||
|
||||
// Update cache
|
||||
const timestamp = Date.now();
|
||||
const cacheEntry: IcsCacheEntry = {
|
||||
sourceId: source.id,
|
||||
events,
|
||||
timestamp,
|
||||
expiresAt: timestamp + this.config.maxCacheAge * 60 * 60 * 1000,
|
||||
};
|
||||
this.cache.set(source.id, cacheEntry);
|
||||
|
||||
// Update sync status
|
||||
this.updateSyncStatus(source.id, {
|
||||
status: "idle",
|
||||
lastSync: timestamp,
|
||||
eventCount: events.length,
|
||||
});
|
||||
|
||||
// Notify listeners
|
||||
this.onEventsUpdated?.(source.id, events);
|
||||
|
||||
// Trigger workspace event so IcsSource can reload
|
||||
try {
|
||||
if (this.plugin?.app?.workspace) {
|
||||
this.plugin.app.workspace.trigger(
|
||||
"task-genius:ics-cache-updated",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"[IcsManager] Failed to trigger ics-cache-updated",
|
||||
e,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
events,
|
||||
errors: [],
|
||||
metadata: {},
|
||||
},
|
||||
timestamp,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
|
||||
console.warn(
|
||||
`[IcsManager] OAuth sync failed for source ${source.id}:`,
|
||||
error,
|
||||
);
|
||||
|
||||
this.updateSyncStatus(source.id, {
|
||||
status: "error",
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status for a source
|
||||
*/
|
||||
|
|
@ -814,6 +1073,41 @@ export class IcsManager extends Component {
|
|||
this.cache.delete(sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache for a specific source (used by OAuth providers like Google Calendar)
|
||||
*/
|
||||
updateCacheForSource(sourceId: string, events: IcsEvent[]): void {
|
||||
const now = Date.now();
|
||||
const cacheEntry: IcsCacheEntry = {
|
||||
sourceId,
|
||||
events,
|
||||
timestamp: now,
|
||||
expiresAt: now + this.config.maxCacheAge * 60 * 60 * 1000,
|
||||
};
|
||||
this.cache.set(sourceId, cacheEntry);
|
||||
|
||||
// Update sync status
|
||||
this.updateSyncStatus(sourceId, {
|
||||
status: "idle",
|
||||
lastSync: now,
|
||||
eventCount: events.length,
|
||||
});
|
||||
|
||||
// Notify listeners
|
||||
this.onEventsUpdated?.(sourceId, events);
|
||||
|
||||
// Trigger workspace event so IcsSource can reload
|
||||
try {
|
||||
if (this.plugin?.app?.workspace) {
|
||||
this.plugin.app.workspace.trigger(
|
||||
"task-genius:ics-cache-updated",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[IcsManager] Failed to trigger ics-cache-updated", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
|
|
@ -824,7 +1118,9 @@ export class IcsManager extends Component {
|
|||
/**
|
||||
* Fetch ICS data from a source
|
||||
*/
|
||||
private async fetchIcsData(source: IcsSource): Promise<IcsFetchResult> {
|
||||
private async fetchIcsData(
|
||||
source: IcsSource | LegacyIcsSource | UrlIcsSourceConfig,
|
||||
): Promise<IcsFetchResult> {
|
||||
try {
|
||||
// Convert webcal URL if needed
|
||||
const conversionResult = WebcalUrlConverter.convertWebcalUrl(
|
||||
|
|
@ -948,14 +1244,19 @@ export class IcsManager extends Component {
|
|||
|
||||
/**
|
||||
* Apply filters to events
|
||||
* @param events - Events to filter
|
||||
* @param source - Source configuration (any calendar source type)
|
||||
*/
|
||||
private applyFilters(events: IcsEvent[], source: IcsSource): IcsEvent[] {
|
||||
private applyFilters(
|
||||
events: IcsEvent[],
|
||||
source: AnyCalendarSource,
|
||||
): IcsEvent[] {
|
||||
let filteredEvents = [...events];
|
||||
console.log("applyFilters: initial events count", events.length);
|
||||
console.log("applyFilters: source config", {
|
||||
showAllDayEvents: source.showAllDayEvents,
|
||||
showTimedEvents: source.showTimedEvents,
|
||||
filters: source.filters,
|
||||
filters: "filters" in source ? source.filters : undefined,
|
||||
});
|
||||
|
||||
// Apply event type filters
|
||||
|
|
@ -974,12 +1275,13 @@ export class IcsManager extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
// Apply custom filters
|
||||
if (source.filters) {
|
||||
// Apply custom filters (only for URL-based sources that have filters)
|
||||
const filters = "filters" in source ? source.filters : undefined;
|
||||
if (filters) {
|
||||
filteredEvents = filteredEvents.filter((event) => {
|
||||
// Include filters
|
||||
if (source.filters!.include) {
|
||||
const include = source.filters!.include;
|
||||
if (filters.include) {
|
||||
const include = filters.include;
|
||||
let shouldInclude = true;
|
||||
|
||||
if (include.summary?.length) {
|
||||
|
|
@ -1018,8 +1320,8 @@ export class IcsManager extends Component {
|
|||
}
|
||||
|
||||
// Exclude filters
|
||||
if (source.filters!.exclude) {
|
||||
const exclude = source.filters!.exclude;
|
||||
if (filters.exclude) {
|
||||
const exclude = filters.exclude;
|
||||
|
||||
if (exclude.summary?.length) {
|
||||
if (
|
||||
|
|
@ -1300,4 +1602,294 @@ export class IcsManager extends Component {
|
|||
this.stopBackgroundRefresh();
|
||||
super.onunload();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Two-Way Sync Methods (for OAuth providers: Google, Outlook, Apple)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Check if a source supports write operations (two-way sync)
|
||||
*/
|
||||
supportsWrite(sourceId: string): boolean {
|
||||
const source = this.config.sources.find((s) => s.id === sourceId);
|
||||
if (!source || !this.calendarSourceManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only OAuth sources support write operations
|
||||
if (!this.isOAuthSource(source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = this.calendarSourceManager.getProvider(
|
||||
source as CalendarSource,
|
||||
);
|
||||
return provider.supportsWrite();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can write to a specific calendar
|
||||
*/
|
||||
canWriteToCalendar(sourceId: string, calendarId?: string): boolean {
|
||||
const source = this.config.sources.find((s) => s.id === sourceId);
|
||||
if (!source || !this.calendarSourceManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isOAuthSource(source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = this.calendarSourceManager.getProvider(
|
||||
source as CalendarSource,
|
||||
);
|
||||
return provider.canWriteToCalendar(calendarId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event in the calendar
|
||||
*
|
||||
* @param sourceId - The calendar source ID
|
||||
* @param event - The event to create
|
||||
* @param calendarId - Optional calendar ID (defaults to primary)
|
||||
* @returns WriteResult with success status and created event
|
||||
*/
|
||||
async createEvent(
|
||||
sourceId: string,
|
||||
event: IcsEvent,
|
||||
calendarId?: string,
|
||||
): Promise<WriteResult> {
|
||||
const source = this.config.sources.find((s) => s.id === sourceId);
|
||||
if (!source) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Source not found: ${sourceId}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.calendarSourceManager) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Calendar Source Manager not initialized",
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isOAuthSource(source)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Source does not support write operations (ICS URLs are read-only)",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = this.calendarSourceManager.getProvider(
|
||||
source as CalendarSource,
|
||||
);
|
||||
const result = await provider.createEvent(event, calendarId);
|
||||
|
||||
// If successful, update local cache
|
||||
if (result.success && result.event) {
|
||||
this.addEventToCache(sourceId, result.event);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.error(
|
||||
`[IcsManager] Failed to create event in source ${sourceId}:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing event in the calendar
|
||||
*
|
||||
* @param sourceId - The calendar source ID
|
||||
* @param options - Update options including the event and optional original event for conflict detection
|
||||
* @returns WriteResult with success status and updated event
|
||||
*/
|
||||
async updateEvent(
|
||||
sourceId: string,
|
||||
options: UpdateEventOptions,
|
||||
): Promise<WriteResult> {
|
||||
const source = this.config.sources.find((s) => s.id === sourceId);
|
||||
if (!source) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Source not found: ${sourceId}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.calendarSourceManager) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Calendar Source Manager not initialized",
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isOAuthSource(source)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Source does not support write operations (ICS URLs are read-only)",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = this.calendarSourceManager.getProvider(
|
||||
source as CalendarSource,
|
||||
);
|
||||
const result = await provider.updateEvent(options);
|
||||
|
||||
// If successful, update local cache
|
||||
if (result.success && result.event) {
|
||||
this.updateEventInCache(sourceId, result.event);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.error(
|
||||
`[IcsManager] Failed to update event in source ${sourceId}:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event from the calendar
|
||||
*
|
||||
* @param sourceId - The calendar source ID
|
||||
* @param eventId - The provider event ID (providerEventId field of IcsEvent)
|
||||
* @param calendarId - Optional calendar ID
|
||||
* @param etag - Optional ETag for conflict detection
|
||||
* @returns WriteResult with success status
|
||||
*/
|
||||
async deleteEvent(
|
||||
sourceId: string,
|
||||
eventId: string,
|
||||
calendarId?: string,
|
||||
etag?: string,
|
||||
): Promise<WriteResult> {
|
||||
const source = this.config.sources.find((s) => s.id === sourceId);
|
||||
if (!source) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Source not found: ${sourceId}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.calendarSourceManager) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Calendar Source Manager not initialized",
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isOAuthSource(source)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Source does not support write operations (ICS URLs are read-only)",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = this.calendarSourceManager.getProvider(
|
||||
source as CalendarSource,
|
||||
);
|
||||
const result = await provider.deleteEvent(
|
||||
eventId,
|
||||
calendarId,
|
||||
etag,
|
||||
);
|
||||
|
||||
// If successful, remove from local cache
|
||||
if (result.success) {
|
||||
this.removeEventFromCache(sourceId, eventId);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
console.error(
|
||||
`[IcsManager] Failed to delete event from source ${sourceId}:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the local cache
|
||||
*/
|
||||
private addEventToCache(sourceId: string, event: IcsEvent): void {
|
||||
const cacheEntry = this.cache.get(sourceId);
|
||||
if (cacheEntry) {
|
||||
cacheEntry.events.push(event);
|
||||
cacheEntry.timestamp = Date.now();
|
||||
|
||||
// Notify listeners
|
||||
this.onEventsUpdated?.(sourceId, cacheEntry.events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an event in the local cache
|
||||
*/
|
||||
private updateEventInCache(sourceId: string, event: IcsEvent): void {
|
||||
const cacheEntry = this.cache.get(sourceId);
|
||||
if (cacheEntry) {
|
||||
const index = cacheEntry.events.findIndex(
|
||||
(e) => e.providerEventId === event.providerEventId,
|
||||
);
|
||||
if (index !== -1) {
|
||||
cacheEntry.events[index] = event;
|
||||
} else {
|
||||
// Event not found in cache, add it
|
||||
cacheEntry.events.push(event);
|
||||
}
|
||||
cacheEntry.timestamp = Date.now();
|
||||
|
||||
// Notify listeners
|
||||
this.onEventsUpdated?.(sourceId, cacheEntry.events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event from the local cache
|
||||
*/
|
||||
private removeEventFromCache(sourceId: string, eventId: string): void {
|
||||
const cacheEntry = this.cache.get(sourceId);
|
||||
if (cacheEntry) {
|
||||
cacheEntry.events = cacheEntry.events.filter(
|
||||
(e) => e.providerEventId !== eventId,
|
||||
);
|
||||
cacheEntry.timestamp = Date.now();
|
||||
|
||||
// Notify listeners
|
||||
this.onEventsUpdated?.(sourceId, cacheEntry.events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,11 +38,6 @@
|
|||
|
||||
/* Global settings */
|
||||
.ics-global-settings {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
|
||||
/* Sources list */
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
/**
|
||||
* ICS (iCalendar) support types and interfaces
|
||||
*
|
||||
* This module contains the core ICS types that are used throughout the plugin.
|
||||
* For the new multi-provider calendar system, see `./calendar-provider.ts`.
|
||||
*
|
||||
* The IcsSource interface is maintained for backward compatibility.
|
||||
* New calendar sources should use CalendarSource from calendar-provider.ts.
|
||||
*/
|
||||
|
||||
import { Task } from "./task";
|
||||
import type {
|
||||
CalendarProviderType,
|
||||
AnyCalendarSource,
|
||||
} from "./calendar-provider";
|
||||
|
||||
/** ICS event source configuration */
|
||||
export interface IcsSource {
|
||||
/** Unique identifier for the ICS source */
|
||||
id: string;
|
||||
/**
|
||||
* Provider type discriminator
|
||||
* @default 'url-ics' - For backward compatibility with legacy sources
|
||||
*/
|
||||
type?: CalendarProviderType;
|
||||
/** Display name for the source */
|
||||
name: string;
|
||||
/** URL to the ICS file (supports http://, https://, and webcal:// protocols) */
|
||||
|
|
@ -231,14 +246,55 @@ export interface IcsEvent {
|
|||
customProperties?: Record<string, string>;
|
||||
/** Source ICS configuration */
|
||||
source: IcsSource;
|
||||
|
||||
// =========================================================================
|
||||
// Sync Metadata (for two-way sync with OAuth providers)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Provider-specific event ID (different from ICS uid)
|
||||
* - Google: event.id
|
||||
* - Outlook: event.id
|
||||
* - Apple: derived from URL path
|
||||
*/
|
||||
providerEventId?: string;
|
||||
|
||||
/**
|
||||
* Calendar ID where this event belongs
|
||||
* - Google: calendarId
|
||||
* - Outlook: calendar.id
|
||||
* - Apple: calendar href
|
||||
*/
|
||||
providerCalendarId?: string;
|
||||
|
||||
/**
|
||||
* ETag for conflict detection (optimistic locking)
|
||||
* Used to detect if remote event was modified since last fetch
|
||||
*/
|
||||
etag?: string;
|
||||
|
||||
/**
|
||||
* Whether this event can be edited (provider supports write + user has permission)
|
||||
*/
|
||||
canEdit?: boolean;
|
||||
|
||||
/**
|
||||
* For recurring events, the ID of the parent recurring event
|
||||
*/
|
||||
recurringEventId?: string;
|
||||
|
||||
/**
|
||||
* Whether this is a single instance of a recurring event
|
||||
*/
|
||||
isRecurringInstance?: boolean;
|
||||
}
|
||||
|
||||
/** ICS event converted to Task format */
|
||||
export interface IcsTask extends Task {
|
||||
/** Original ICS event data */
|
||||
icsEvent: IcsEvent;
|
||||
/** Whether this task is read-only (from ICS) */
|
||||
readonly: true;
|
||||
/** Whether this task is read-only (true for URL ICS, false for writable OAuth providers) */
|
||||
readonly: boolean;
|
||||
/** Whether this task is a badge */
|
||||
badge: boolean;
|
||||
/** Source information */
|
||||
|
|
@ -246,6 +302,8 @@ export interface IcsTask extends Task {
|
|||
type: "ics";
|
||||
name: string;
|
||||
id: string;
|
||||
/** Provider type for determining write capability */
|
||||
providerType?: "url-ics" | "google" | "outlook" | "apple-caldav";
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -306,8 +364,12 @@ export interface IcsCacheEntry {
|
|||
|
||||
/** ICS manager configuration */
|
||||
export interface IcsManagerConfig {
|
||||
/** List of ICS sources */
|
||||
sources: IcsSource[];
|
||||
/**
|
||||
* List of calendar sources (supports all provider types)
|
||||
* Uses AnyCalendarSource to accept both legacy IcsSource and new CalendarSource formats
|
||||
* Call normalizeCalendarSources() when reading to ensure consistent format
|
||||
*/
|
||||
sources: AnyCalendarSource[];
|
||||
/** Global refresh interval in minutes */
|
||||
globalRefreshInterval: number;
|
||||
/** Maximum cache age in hours */
|
||||
|
|
|
|||
Loading…
Reference in a new issue