From d076635952fe4688fd9905f8e51f4b18420d1279 Mon Sep 17 00:00:00 2001 From: Lorite Date: Fri, 6 Mar 2026 11:22:01 +0100 Subject: [PATCH 1/2] Automatically syncing timeblocks to external calendar --- docs/releases/unreleased.md | 4 + src/bases/calendar-core.ts | 28 ++ src/bootstrap/pluginBootstrap.ts | 5 + src/bootstrap/pluginRuntime.ts | 1 + src/main.ts | 3 + src/modals/TimeblockCreationModal.ts | 12 + src/modals/TimeblockInfoModal.ts | 24 +- src/services/TimeblockCalendarSyncService.ts | 393 +++++++++++++++++++ src/settings/defaults.ts | 1 + src/settings/settingsPersistence.ts | 4 + src/settings/tabs/integrationsTab.ts | 12 + src/types.ts | 1 + src/types/settings.ts | 1 + 13 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 src/services/TimeblockCalendarSyncService.ts diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 121110a03..37cbfa118 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -31,3 +31,7 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l ``` --> + +## Added + +- Added a dedicated Google Calendar integration setting to enable or disable timeblock synchronization independently from task synchronization diff --git a/src/bases/calendar-core.ts b/src/bases/calendar-core.ts index 1b0193078..1efa73d4a 100644 --- a/src/bases/calendar-core.ts +++ b/src/bases/calendar-core.ts @@ -1833,6 +1833,20 @@ export async function handleTimeblockDrop( newEndTime ); + if (plugin.timeblockCalendarSyncService?.isEnabled()) { + const updatedTimeblock: TimeBlock = { + ...timeblock, + startTime: newStartTime, + endTime: newEndTime, + }; + + plugin.timeblockCalendarSyncService + .updateTimeblockInCalendar(updatedTimeblock, newDate, originalDate) + .catch((error) => { + console.warn("Failed to sync moved timeblock to Google Calendar:", error); + }); + } + new Notice("Timeblock moved successfully"); } catch (error: unknown) { tasknotesLogger.error("Error moving timeblock:", { @@ -1877,6 +1891,20 @@ export async function handleTimeblockResize( newEndTime ); + if (plugin.timeblockCalendarSyncService?.isEnabled()) { + const updatedTimeblock: TimeBlock = { + ...timeblock, + startTime: newStartTime, + endTime: newEndTime, + }; + + plugin.timeblockCalendarSyncService + .updateTimeblockInCalendar(updatedTimeblock, originalDate) + .catch((error) => { + console.warn("Failed to sync resized timeblock to Google Calendar:", error); + }); + } + new Notice("Timeblock duration updated"); } catch (error: unknown) { tasknotesLogger.error("Error resizing timeblock:", { diff --git a/src/bootstrap/pluginBootstrap.ts b/src/bootstrap/pluginBootstrap.ts index 28747311a..2c6321505 100644 --- a/src/bootstrap/pluginBootstrap.ts +++ b/src/bootstrap/pluginBootstrap.ts @@ -355,6 +355,11 @@ export function initializeServicesLazily(plugin: TaskNotesPlugin): void { await plugin.taskCalendarSyncService.initializeExternalFileReconciliation(); plugin.taskCalendarSyncService.startRecoveryQueueProcessor(); + // Initialize Timeblock Calendar Sync service for pushing timeblocks to Google Calendar + plugin.timeblockCalendarSyncService = new ( + await import("../services/TimeblockCalendarSyncService") + ).TimeblockCalendarSyncService(plugin, plugin.googleCalendarService); + plugin.registerEvent( plugin.emitter.on("file-updated", (data: FileUpdatedEventData) => { if (!plugin.taskCalendarSyncService || !data?.path) { diff --git a/src/bootstrap/pluginRuntime.ts b/src/bootstrap/pluginRuntime.ts index d56b93e9c..ad369a176 100644 --- a/src/bootstrap/pluginRuntime.ts +++ b/src/bootstrap/pluginRuntime.ts @@ -75,6 +75,7 @@ export async function cleanupPluginRuntime(plugin: TaskNotesPlugin): Promise { + console.warn("Failed to sync timeblock to Google Calendar:", error); + }); + } + // Refresh calendar views this.plugin.emitter.trigger("data-changed"); void this.options.onCreated?.({ diff --git a/src/modals/TimeblockInfoModal.ts b/src/modals/TimeblockInfoModal.ts index 83390c774..11ea45beb 100644 --- a/src/modals/TimeblockInfoModal.ts +++ b/src/modals/TimeblockInfoModal.ts @@ -51,7 +51,8 @@ export interface TimeBlock { description?: string; attachments?: string[]; color?: string; - id?: string; + id: string; + googleCalendarEventId?: string; } /** @@ -469,6 +470,18 @@ export class TimeblockInfoModal extends Modal { // Save to daily note await this.updateTimeblockInDailyNote(); + // Sync to Google Calendar if enabled (fire-and-forget to avoid blocking modal) + if ( + this.plugin.timeblockCalendarSyncService?.isEnabled() && + this.plugin.settings.googleCalendarExport.syncOnTaskUpdate + ) { + this.plugin.timeblockCalendarSyncService + .updateTimeblockInCalendar(this.timeblock, this.timeblockDate) + .catch((error) => { + console.warn("Failed to sync timeblock update to Google Calendar:", error); + }); + } + // Signal immediate update before triggering data change this.onChange?.(); @@ -563,6 +576,15 @@ export class TimeblockInfoModal extends Modal { if (!confirmed) return; try { + // Delete from Google Calendar before deleting from daily note so we still have ID linkage + if (this.plugin.timeblockCalendarSyncService?.isEnabled()) { + this.plugin.timeblockCalendarSyncService + .deleteTimeblockFromCalendar(this.timeblock, this.timeblockDate) + .catch((error) => { + console.warn("Failed to delete timeblock from Google Calendar:", error); + }); + } + await this.deleteTimeblockFromDailyNote(); // Signal immediate update before triggering data change diff --git a/src/services/TimeblockCalendarSyncService.ts b/src/services/TimeblockCalendarSyncService.ts new file mode 100644 index 000000000..069194e51 --- /dev/null +++ b/src/services/TimeblockCalendarSyncService.ts @@ -0,0 +1,393 @@ +import { Notice, TFile } from "obsidian"; +import { + appHasDailyNotesPluginLoaded, + getAllDailyNotes, + getDailyNote, +} from "obsidian-daily-notes-interface"; +import TaskNotesPlugin from "../main"; +import { TimeBlock } from "../types"; +import { GoogleCalendarService } from "./GoogleCalendarService"; + +/** Debounce delay for rapid timeblock updates (ms) */ +const SYNC_DEBOUNCE_MS = 500; + +/** Max concurrent API calls during bulk sync to avoid rate limits */ +const SYNC_CONCURRENCY_LIMIT = 5; + +interface TimeblockSyncResult { + synced: number; + failed: number; + skipped: number; +} + +export class TimeblockCalendarSyncService { + private plugin: TaskNotesPlugin; + private googleCalendarService: GoogleCalendarService; + + /** Debounce timers for pending syncs, keyed by date + timeblock ID */ + private pendingSyncs: Map = new Map(); + + constructor(plugin: TaskNotesPlugin, googleCalendarService: GoogleCalendarService) { + this.plugin = plugin; + this.googleCalendarService = googleCalendarService; + } + + destroy(): void { + for (const timer of this.pendingSyncs.values()) { + window.clearTimeout(timer); + } + this.pendingSyncs.clear(); + } + + isEnabled(): boolean { + const settings = this.plugin.settings.googleCalendarExport; + const enabled = settings.enabled; + const timeblockSyncEnabled = settings.syncTimeblocks; + const hasTargetCalendar = !!settings.targetCalendarId; + const isConnected = this.googleCalendarService.getAvailableCalendars().length > 0; + + return enabled && timeblockSyncEnabled && hasTargetCalendar && isConnected; + } + + private getEventId(timeblock: TimeBlock): string | undefined { + return timeblock.googleCalendarEventId; + } + + private getSyncKey(timeblock: TimeBlock, date: string): string { + if (timeblock.id) { + return `${date}:${timeblock.id}`; + } + + return `${date}:${timeblock.title}:${timeblock.startTime}:${timeblock.endTime}`; + } + + private buildEventDescription(timeblock: TimeBlock, date: string): string | undefined { + if (!this.plugin.settings.googleCalendarExport.includeDescription) { + return undefined; + } + + const lines: string[] = []; + if (timeblock.description) { + lines.push(timeblock.description); + } + + if (timeblock.attachments && timeblock.attachments.length > 0) { + lines.push(""); + lines.push("Attachments:"); + for (const attachment of timeblock.attachments) { + lines.push(`- ${attachment}`); + } + } + + if (this.plugin.settings.googleCalendarExport.includeObsidianLink) { + const dailyNote = this.getDailyNoteForDate(date); + if (dailyNote) { + const vaultName = this.plugin.app.vault.getName(); + const encodedPath = encodeURIComponent(dailyNote.path); + const obsidianUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodedPath}`; + if (lines.length > 0) { + lines.push(""); + lines.push("---"); + } + lines.push(`Open Daily Note in Obsidian`); + } + } + + return lines.length > 0 ? lines.join("\n") : undefined; + } + + private toCalendarEvent(timeblock: TimeBlock, date: string): { + summary: string; + description?: string; + start: { dateTime: string; timeZone: string }; + end: { dateTime: string; timeZone: string }; + colorId?: string; + } { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const startDateTime = `${date}T${timeblock.startTime}:00`; + const endDateTime = `${date}T${timeblock.endTime}:00`; + + const event: { + summary: string; + description?: string; + start: { dateTime: string; timeZone: string }; + end: { dateTime: string; timeZone: string }; + colorId?: string; + } = { + summary: timeblock.title || "Timeblock", + start: { dateTime: startDateTime, timeZone: timezone }, + end: { dateTime: endDateTime, timeZone: timezone }, + }; + + const description = this.buildEventDescription(timeblock, date); + if (description) { + event.description = description; + } + + if (this.plugin.settings.googleCalendarExport.eventColorId) { + event.colorId = this.plugin.settings.googleCalendarExport.eventColorId; + } + + return event; + } + + private extractEventId(icsEventId: string): string { + const eventIdMatch = icsEventId.match(/^google-[^-]+-(.+)$/); + return eventIdMatch ? eventIdMatch[1] : icsEventId; + } + + private getDailyNoteForDate(date: string): TFile | null { + if (!appHasDailyNotesPluginLoaded()) { + return null; + } + + const moment = (window as any).moment(date, "YYYY-MM-DD"); + const allDailyNotes = getAllDailyNotes(); + const dailyNote = getDailyNote(moment, allDailyNotes); + + return dailyNote instanceof TFile ? dailyNote : null; + } + + private findTimeblockIndex(timeblocks: any[], timeblock: TimeBlock): number { + if (timeblock.id) { + const idIndex = timeblocks.findIndex((tb) => tb?.id === timeblock.id); + if (idIndex >= 0) { + return idIndex; + } + } + + return timeblocks.findIndex( + (tb) => + tb?.title === timeblock.title && + tb?.startTime === timeblock.startTime && + tb?.endTime === timeblock.endTime + ); + } + + private async setTimeblockEventId( + date: string, + timeblock: TimeBlock, + eventId?: string + ): Promise { + const dailyNote = this.getDailyNoteForDate(date); + if (!dailyNote) { + return; + } + + await this.plugin.app.fileManager.processFrontMatter(dailyNote, (frontmatter) => { + if (!frontmatter.timeblocks || !Array.isArray(frontmatter.timeblocks)) { + return; + } + + const index = this.findTimeblockIndex(frontmatter.timeblocks, timeblock); + if (index === -1) { + return; + } + + if (eventId) { + frontmatter.timeblocks[index].googleCalendarEventId = eventId; + } else { + delete frontmatter.timeblocks[index].googleCalendarEventId; + } + }); + } + + private async processInParallel( + items: T[], + processor: (item: T) => Promise + ): Promise { + const executing: Promise[] = []; + + for (const item of items) { + const promise = processor(item).then(() => { + executing.splice(executing.indexOf(promise), 1); + }); + executing.push(promise); + + if (executing.length >= SYNC_CONCURRENCY_LIMIT) { + await Promise.race(executing); + } + } + + await Promise.all(executing); + } + + async syncTimeblockToCalendar(timeblock: TimeBlock, date: string): Promise { + if (!this.isEnabled()) { + return; + } + + const settings = this.plugin.settings.googleCalendarExport; + const existingEventId = this.getEventId(timeblock); + const eventData = this.toCalendarEvent(timeblock, date); + + try { + if (existingEventId) { + await this.googleCalendarService.updateEvent( + settings.targetCalendarId, + existingEventId, + eventData + ); + return; + } + + const createdEvent = await this.googleCalendarService.createEvent( + settings.targetCalendarId, + { + ...eventData, + start: eventData.start, + end: eventData.end, + isAllDay: false, + } + ); + + const eventId = this.extractEventId(createdEvent.id); + await this.setTimeblockEventId(date, timeblock, eventId); + timeblock.googleCalendarEventId = eventId; + } catch (error: any) { + if (error.status === 404 && existingEventId) { + await this.setTimeblockEventId(date, timeblock, undefined); + timeblock.googleCalendarEventId = undefined; + return this.syncTimeblockToCalendar(timeblock, date); + } + + console.error("[TimeblockCalendarSync] Failed to sync timeblock:", { + date, + timeblockId: timeblock.id, + error, + }); + } + } + + async updateTimeblockInCalendar( + timeblock: TimeBlock, + date: string, + previousDate?: string + ): Promise { + if (!this.plugin.settings.googleCalendarExport.syncOnTaskUpdate) { + return; + } + + const key = this.getSyncKey(timeblock, previousDate || date); + const existingTimer = this.pendingSyncs.get(key); + if (existingTimer) { + window.clearTimeout(existingTimer); + } + + return new Promise((resolve) => { + const timer = window.setTimeout(async () => { + this.pendingSyncs.delete(key); + await this.syncTimeblockToCalendar(timeblock, date); + resolve(); + }, SYNC_DEBOUNCE_MS); + + this.pendingSyncs.set(key, timer); + }); + } + + async deleteTimeblockFromCalendar(timeblock: TimeBlock, date: string): Promise { + if (!this.plugin.settings.googleCalendarExport.syncOnTaskDelete) { + return; + } + + if (!this.isEnabled()) { + return; + } + + const existingEventId = this.getEventId(timeblock); + if (!existingEventId) { + return; + } + + const settings = this.plugin.settings.googleCalendarExport; + try { + await this.googleCalendarService.deleteEvent(settings.targetCalendarId, existingEventId); + } catch (error: any) { + if (error.status !== 404 && error.status !== 410) { + console.error("[TimeblockCalendarSync] Failed to delete event:", error); + } + } + + await this.setTimeblockEventId(date, timeblock, undefined); + timeblock.googleCalendarEventId = undefined; + } + + async syncAllTimeblocks(): Promise { + const results: TimeblockSyncResult = { synced: 0, failed: 0, skipped: 0 }; + + if (!this.isEnabled()) { + new Notice("Google Calendar export is not enabled or configured."); + return results; + } + + if (!appHasDailyNotesPluginLoaded()) { + new Notice("Daily Notes plugin is not enabled."); + return results; + } + + const allDailyNotes = getAllDailyNotes(); + const syncItems: Array<{ date: string; timeblock: TimeBlock }> = []; + + for (const [dateKey, dailyNote] of Object.entries(allDailyNotes)) { + if (!(dailyNote instanceof TFile)) { + continue; + } + + const date = this.normalizeDateKey(dateKey); + if (!date) { + continue; + } + + const cache = this.plugin.app.metadataCache.getFileCache(dailyNote); + const timeblocks = cache?.frontmatter?.timeblocks; + if (!timeblocks || !Array.isArray(timeblocks)) { + continue; + } + + for (const timeblock of timeblocks) { + if (!timeblock || typeof timeblock.startTime !== "string" || typeof timeblock.endTime !== "string") { + results.skipped++; + continue; + } + + syncItems.push({ date, timeblock: timeblock as TimeBlock }); + } + } + + new Notice(`Syncing ${syncItems.length} timeblocks to Google Calendar...`); + + await this.processInParallel(syncItems, async ({ date, timeblock }) => { + try { + await this.syncTimeblockToCalendar(timeblock, date); + results.synced++; + } catch (error) { + results.failed++; + console.error("[TimeblockCalendarSync] Failed sync item:", { date, timeblock, error }); + } + }); + + new Notice( + `Timeblock sync complete. Synced: ${results.synced}, Failed: ${results.failed}, Skipped: ${results.skipped}` + ); + return results; + } + + private normalizeDateKey(dateKey: string): string | null { + const moment = (window as any).moment; + if (!moment) { + return null; + } + + const strict = moment(dateKey, ["YYYY-MM-DD", "YYYYMMDD", "MM-DD-YYYY", "DD-MM-YYYY"], true); + if (strict.isValid()) { + return strict.format("YYYY-MM-DD"); + } + + const relaxed = moment(dateKey); + if (relaxed.isValid()) { + return relaxed.format("YYYY-MM-DD"); + } + + return null; + } +} diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 106920c5d..ff260072d 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -191,6 +191,7 @@ export const DEFAULT_ICS_INTEGRATION_SETTINGS: ICSIntegrationSettings = { export const DEFAULT_GOOGLE_CALENDAR_EXPORT: GoogleCalendarExportSettings = { enabled: false, // Disabled by default - user must opt-in + syncTimeblocks: false, // Timeblock sync is opt-in targetCalendarId: "", // Empty = user must select a calendar syncOnTaskCreate: true, syncOnTaskUpdate: true, diff --git a/src/settings/settingsPersistence.ts b/src/settings/settingsPersistence.ts index a04d3c5bf..6affcc9f0 100644 --- a/src/settings/settingsPersistence.ts +++ b/src/settings/settingsPersistence.ts @@ -222,6 +222,10 @@ export function buildSettingsFromLoadedData(data: LoadedSettingsData | null): Se ...DEFAULT_SETTINGS.icsIntegration, ...(loadedData?.icsIntegration || {}), }, + googleCalendarExport: { + ...DEFAULT_SETTINGS.googleCalendarExport, + ...(loadedData?.googleCalendarExport || {}), + }, nlpTriggers: { ...DEFAULT_SETTINGS.nlpTriggers, ...(loadedData?.nlpTriggers || {}), diff --git a/src/settings/tabs/integrationsTab.ts b/src/settings/tabs/integrationsTab.ts index 4fde3b504..7a5430902 100644 --- a/src/settings/tabs/integrationsTab.ts +++ b/src/settings/tabs/integrationsTab.ts @@ -1111,6 +1111,18 @@ export function renderIntegrationsTab( }) ); + group.addSetting((setting) => + configureToggleSetting(setting, { + name: "Sync timeblocks", + desc: "Enable syncing timeblocks to Google Calendar.", + getValue: () => plugin.settings.googleCalendarExport.syncTimeblocks, + setValue: async (value: boolean) => { + plugin.settings.googleCalendarExport.syncTimeblocks = value; + save(); + }, + }) + ); + // Manual sync actions - section header group.addSetting((setting) => { setting.setName( diff --git a/src/types.ts b/src/types.ts index c968e6bea..89d9caaab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -536,6 +536,7 @@ export interface TimeBlock { attachments?: string[]; // Optional array of markdown links to tasks/notes color?: string; // Optional hex color for display description?: string; // Optional description + googleCalendarEventId?: string; // Optional linked Google Calendar event ID } // Note types diff --git a/src/types/settings.ts b/src/types/settings.ts index 960391ce5..9d731a93a 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -328,6 +328,7 @@ export interface ICSIntegrationSettings { */ export interface GoogleCalendarExportSettings { enabled: boolean; // Master enable/disable for task export + syncTimeblocks: boolean; // Enable syncing timeblocks to Google Calendar targetCalendarId: string; // Which calendar to create events in syncOnTaskCreate: boolean; // Auto-sync when task is created syncOnTaskUpdate: boolean; // Auto-sync when task is updated From c531c8dd4f175e1ba2279f091fdbaceff87c1cbc Mon Sep 17 00:00:00 2001 From: Lorite Date: Sun, 8 Mar 2026 15:48:23 +0100 Subject: [PATCH 2/2] Modify timeblocks exported calendar title --- docs/releases/unreleased.md | 1 + src/services/TimeblockCalendarSyncService.ts | 26 +++++++++++++++++++- src/settings/defaults.ts | 1 + src/settings/tabs/integrationsTab.ts | 14 +++++++++++ src/types/settings.ts | 1 + 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 37cbfa118..bae5fdeb8 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -34,4 +34,5 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l ## Added +- Added a dedicated "Timeblock Event Title Template" setting so synced timeblock event names can differ from task event names - Added a dedicated Google Calendar integration setting to enable or disable timeblock synchronization independently from task synchronization diff --git a/src/services/TimeblockCalendarSyncService.ts b/src/services/TimeblockCalendarSyncService.ts index 069194e51..15f8407dc 100644 --- a/src/services/TimeblockCalendarSyncService.ts +++ b/src/services/TimeblockCalendarSyncService.ts @@ -96,6 +96,30 @@ export class TimeblockCalendarSyncService { return lines.length > 0 ? lines.join("\n") : undefined; } + /** + * Apply the shared Google Calendar event title template to a timeblock. + * Supports task placeholders for compatibility and adds timeblock-specific fields. + */ + private applyTitleTemplate(timeblock: TimeBlock, date: string): string { + const settings = this.plugin.settings.googleCalendarExport; + const template = + settings.timeblockEventTitleTemplate || settings.eventTitleTemplate || "{{title}}"; + const fallbackTitle = timeblock.title || "Timeblock"; + + const rendered = template + .replace(/\{\{title\}\}/g, fallbackTitle) + .replace(/\{\{status\}\}/g, "") + .replace(/\{\{priority\}\}/g, "") + .replace(/\{\{due\}\}/g, "") + .replace(/\{\{scheduled\}\}/g, "") + .replace(/\{\{date\}\}/g, date) + .replace(/\{\{startTime\}\}/g, timeblock.startTime || "") + .replace(/\{\{endTime\}\}/g, timeblock.endTime || "") + .trim(); + + return rendered || fallbackTitle; + } + private toCalendarEvent(timeblock: TimeBlock, date: string): { summary: string; description?: string; @@ -114,7 +138,7 @@ export class TimeblockCalendarSyncService { end: { dateTime: string; timeZone: string }; colorId?: string; } = { - summary: timeblock.title || "Timeblock", + summary: this.applyTitleTemplate(timeblock, date), start: { dateTime: startDateTime, timeZone: timezone }, end: { dateTime: endDateTime, timeZone: timezone }, }; diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index ff260072d..0b3ef9004 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -198,6 +198,7 @@ export const DEFAULT_GOOGLE_CALENDAR_EXPORT: GoogleCalendarExportSettings = { syncOnTaskComplete: true, syncOnTaskDelete: true, eventTitleTemplate: "{{title}}", // Simple title by default + timeblockEventTitleTemplate: "{{title}}", // Timeblock title by default includeDescription: true, eventColorId: null, // Use calendar default color syncTrigger: "scheduled", // Default to scheduled date diff --git a/src/settings/tabs/integrationsTab.ts b/src/settings/tabs/integrationsTab.ts index 7a5430902..5a876f6ed 100644 --- a/src/settings/tabs/integrationsTab.ts +++ b/src/settings/tabs/integrationsTab.ts @@ -974,6 +974,20 @@ export function renderIntegrationsTab( }) ); + // Timeblock event title template + group.addSetting((setting) => + configureTextSetting(setting, { + name: "Timeblock Event Title Template", + desc: "Template for synced timeblock event titles (used when Sync timeblocks is enabled). Supports {{title}}, {{date}}, {{startTime}}, {{endTime}}.", + placeholder: "{{title}}", + getValue: () => plugin.settings.googleCalendarExport.timeblockEventTitleTemplate, + setValue: async (value: string) => { + plugin.settings.googleCalendarExport.timeblockEventTitleTemplate = value || "{{title}}"; + save(); + }, + }) + ); + // Include description group.addSetting( (setting) => diff --git a/src/types/settings.ts b/src/types/settings.ts index 9d731a93a..3e3edc653 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -335,6 +335,7 @@ export interface GoogleCalendarExportSettings { syncOnTaskComplete: boolean; // Update event when task is completed syncOnTaskDelete: boolean; // Delete event when task is deleted eventTitleTemplate: string; // Template for event title (e.g., "{{title}}" or "[TaskNotes] {{title}}") + timeblockEventTitleTemplate: string; // Template for timeblock event title includeDescription: boolean; // Include task details in event description eventColorId: string | null; // Optional: Google Calendar color ID for TaskNotes events (null = calendar default) syncTrigger: "scheduled" | "due" | "both"; // Which date triggers event creation