Skip to content

Commit 97f28e7

Browse files
committed
feat(calendar): add Apple CalDAV two-way sync with write operations
- Implement createEvent, updateEvent, deleteEvent methods in AppleCaldavProvider - Add ICS generation for CalDAV PUT requests with proper RFC 5545 escaping - Extract event metadata (href, etag) for optimistic locking and conflict detection - Add syncAppleSource method to CalendarSettingsTab - Mark Google, Outlook, Apple CalDAV providers as "Coming Soon" in source modal perf(kanban): cache CSS custom property tag colors - Prevent layout thrashing from repeated getComputedStyle calls - Clear cache on css-change event (theme switch, snippet toggle)
1 parent d1f37fc commit 97f28e7

File tree

4 files changed

+697
-76
lines changed

4 files changed

+697
-76
lines changed

src/components/features/kanban/kanban-card.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ import { createTaskCheckbox } from "@/components/features/task/view/details";
77
import { getEffectiveProject } from "@/utils/task/task-operations";
88
import { sanitizePriorityForClass } from "@/utils/task/priority-utils";
99

10+
/**
11+
* Cache for CSS custom property tag colors to prevent layout thrashing.
12+
* Reading getComputedStyle() in a render loop causes forced synchronous reflow.
13+
* This cache stores resolved tag colors so we only read the DOM once per tag.
14+
*/
15+
const tagColorCache: Map<string, string | null> = new Map();
16+
17+
/**
18+
* Clears the tag color cache. Should be called when CSS changes
19+
* (e.g., theme switch, CSS snippet toggle) to ensure fresh values.
20+
*/
21+
export function clearTagColorCache(): void {
22+
tagColorCache.clear();
23+
}
24+
1025
export class KanbanCardComponent extends Component {
1126
public element: HTMLElement;
1227
private task: Task;
@@ -317,14 +332,24 @@ export class KanbanCardComponent extends Component {
317332
const color = tagColors[tagName];
318333
tagEl.style.setProperty("--tag-color", color);
319334
tagEl.classList.add("colored-tag");
335+
return; // Color found from plugin, no need to check CSS vars
320336
}
321337
}
322338

323339
// Fallback: check for CSS custom properties set by other tag color plugins
324-
const computedStyle = getComputedStyle(document.body);
325-
const tagColorVar = computedStyle.getPropertyValue(
326-
`--tag-color-${tagName}`,
327-
);
340+
// Use cache to prevent layout thrashing from repeated getComputedStyle calls
341+
let tagColorVar = tagColorCache.get(tagName);
342+
343+
if (tagColorVar === undefined) {
344+
// Cache miss: read from DOM once and cache the result
345+
const computedStyle = getComputedStyle(document.body);
346+
const val = computedStyle
347+
.getPropertyValue(`--tag-color-${tagName}`)
348+
.trim();
349+
tagColorVar = val || null;
350+
tagColorCache.set(tagName, tagColorVar);
351+
}
352+
328353
if (tagColorVar) {
329354
tagEl.style.setProperty("--tag-color", tagColorVar);
330355
tagEl.classList.add("colored-tag");

src/components/features/kanban/kanban.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import TaskProgressBarPlugin from "@/index"; // Adjust path as needed
1010
import { Task } from "@/types/task"; // Adjust path as needed
1111
import { KanbanColumnComponent } from "./kanban-column";
12+
import { clearTagColorCache } from "./kanban-card";
1213
// import { DragManager, DragMoveEvent, DragEndEvent } from "@/components/ui/behavior/DragManager";
1314
import Sortable from "sortablejs";
1415
import "@/styles/kanban/kanban.scss";
@@ -136,6 +137,13 @@ export class KanbanComponent extends Component {
136137
this.containerEl.empty();
137138
this.containerEl.addClass("tg-kanban-view");
138139

140+
// Clear tag color cache on CSS changes (theme switch, snippet toggle)
141+
this.registerEvent(
142+
this.app.workspace.on("css-change", () => {
143+
clearTagColorCache();
144+
})
145+
);
146+
139147
// Load configuration settings
140148
this.loadKanbanConfig();
141149
this.loadCycleSelection();

src/components/features/settings/tabs/CalendarSettingsTab.ts

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ export class CalendarSettingsComponent {
319319
} else if (isOutlookSource(source)) {
320320
// Use OutlookCalendarProvider for Outlook sources
321321
await this.syncOutlookSource(source);
322+
} else if (isAppleSource(source)) {
323+
// Use AppleCaldavProvider for Apple CalDAV sources
324+
await this.syncAppleSource(source);
322325
} else if (isUrlIcsSource(source)) {
323326
// Use IcsManager for URL-based sources
324327
const icsManager = this.plugin.getIcsManager();
@@ -520,6 +523,49 @@ export class CalendarSettingsComponent {
520523

521524
new Notice(t("Sync completed") + `: ${events.length} ` + t("events"));
522525
}
526+
527+
/**
528+
* Sync an Apple CalDAV source using AppleCaldavProvider
529+
*/
530+
private async syncAppleSource(
531+
source: AppleCaldavSourceConfig,
532+
): Promise<void> {
533+
if (!source.appSpecificPassword) {
534+
throw new Error("Not authenticated with iCloud Calendar");
535+
}
536+
537+
if (!source.calendarHrefs || source.calendarHrefs.length === 0) {
538+
throw new Error("No calendars selected for sync");
539+
}
540+
541+
// Import the provider dynamically to avoid circular deps
542+
const { AppleCaldavProvider } =
543+
await import("@/providers/apple-caldav-provider");
544+
545+
// Create provider instance
546+
const provider = new AppleCaldavProvider(source);
547+
548+
// Fetch events for the configured date range (default: last 30 days to next 90 days)
549+
const now = new Date();
550+
const start = new Date(now);
551+
start.setDate(start.getDate() - 30);
552+
const end = new Date(now);
553+
end.setDate(end.getDate() + 90);
554+
555+
const events = await provider.getEvents({
556+
range: { start, end },
557+
expandRecurring: true,
558+
});
559+
560+
// Update cache in IcsManager
561+
const icsManager = this.plugin.getIcsManager();
562+
if (icsManager && events.length > 0) {
563+
// Store events in the IcsManager cache
564+
icsManager.updateCacheForSource(source.id, events);
565+
}
566+
567+
new Notice(t("Sync completed") + `: ${events.length} ` + t("events"));
568+
}
523569
}
524570

525571
// ============================================================================
@@ -621,19 +667,33 @@ class CalendarSourceModal extends Modal {
621667
"apple-caldav",
622668
];
623669

670+
// Temporarily disabled providers (not yet fully implemented)
671+
const disabledProviders: CalendarProviderType[] = [
672+
"google",
673+
"outlook",
674+
"apple-caldav",
675+
];
676+
624677
for (const type of providerTypes) {
625678
const meta = CalendarProviderMeta[type];
626-
const card = grid.createDiv("type-card");
679+
const isDisabled = disabledProviders.includes(type);
680+
const card = grid.createDiv({
681+
cls: `type-card${isDisabled ? " type-card-disabled" : ""}`,
682+
});
627683

628684
const iconDiv = card.createDiv("type-icon");
629685
setIcon(iconDiv, meta.icon);
630686

631687
card.createDiv("type-name").setText(meta.displayName);
632688
card.createDiv("type-desc").setText(meta.description);
633689

634-
card.onclick = () => {
635-
this.selectType(type);
636-
};
690+
if (isDisabled) {
691+
card.createDiv("type-badge").setText(t("Coming Soon"));
692+
} else {
693+
card.onclick = () => {
694+
this.selectType(type);
695+
};
696+
}
637697
}
638698
}
639699

0 commit comments

Comments
 (0)