mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Webhooks enable real-time integrations by sending HTTP POST requests to
configured
endpoints when TaskNotes events occur. This opens up powerful automation
and
sync capabilities for external services.
**New Webhook Features:**
- Complete webhook management system with CRUD operations
- 12+ event types covering tasks, time tracking, and pomodoro sessions
- HMAC-SHA256 signature verification for security
- Automatic retries with exponential backoff
- Delivery tracking and failure monitoring
- Auto-disable after repeated failures
**Event Types:**
- Task events: created, updated, deleted, completed, archived,
unarchived
- Time tracking: started, stopped
- Pomodoro: started, completed, interrupted
- Recurring: instance completed
**API Endpoints:**
- POST /api/webhooks - Register new webhook
- GET /api/webhooks - List configured webhooks
- DELETE /api/webhooks/{id} - Remove webhook
- GET /api/webhooks/deliveries - View delivery history
**Settings Integration:**
- Webhook management UI in HTTP API settings tab
- Event subscription selection
- Enable/disable individual webhooks
- Success/failure statistics display
**Security & Reliability:**
- HMAC signatures for payload verification
- 10-second timeout per delivery
- 3 automatic retries with backoff
- Webhook secrets auto-generated
- Non-blocking async delivery
**Documentation:**
- Comprehensive webhook guide with examples
- Integration patterns for popular services
- Security best practices
- Testing utilities and sample code
**Testing:**
- Webhook test server (test-webhook.js)
- Signature verification examples
- Debug logging and monitoring
This enables powerful integrations like:
- Real-time sync with Todoist, TickTick, Notion
- Slack/Discord notifications
- Time tracking integration (Toggl, Clockify)
- Custom automation workflows
- Analytics and reporting dashboards
The webhook system is designed to be reliable, secure, and performant
while
maintaining TaskNotes' core responsiveness.
3626 lines
126 KiB
TypeScript
3626 lines
126 KiB
TypeScript
import { App, PluginSettingTab, Setting, Notice, setIcon, TAbstractFile, TFile, setTooltip, Platform, Modal } from 'obsidian';
|
||
import TaskNotesPlugin from '../main';
|
||
import { FieldMapping, StatusConfig, PriorityConfig, SavedView, Reminder, TaskInfo, WebhookConfig } from '../types';
|
||
import { StatusManager } from '../services/StatusManager';
|
||
import { PriorityManager } from '../services/PriorityManager';
|
||
import { showConfirmationModal } from '../modals/ConfirmationModal';
|
||
import { showStorageLocationConfirmationModal } from '../modals/StorageLocationConfirmationModal';
|
||
import { ProjectSelectModal } from '../modals/ProjectSelectModal';
|
||
|
||
export interface TaskNotesSettings {
|
||
tasksFolder: string; // Now just a default location for new tasks
|
||
taskTag: string; // The tag that identifies tasks
|
||
taskIdentificationMethod: 'tag' | 'property'; // Method to identify tasks
|
||
taskPropertyName: string; // Property name for property-based identification
|
||
taskPropertyValue: string; // Property value for property-based identification
|
||
excludedFolders: string; // Comma-separated list of folders to exclude from Notes tab
|
||
defaultTaskPriority: string; // Changed to string to support custom priorities
|
||
defaultTaskStatus: string; // Changed to string to support custom statuses
|
||
taskOrgFiltersCollapsed: boolean; // Save collapse state of task organization filters
|
||
// Task filename settings
|
||
taskFilenameFormat: 'title' | 'zettel' | 'timestamp' | 'custom';
|
||
storeTitleInFilename: boolean;
|
||
customFilenameTemplate: string; // Template for custom format
|
||
// Task creation defaults
|
||
taskCreationDefaults: TaskCreationDefaults;
|
||
// Calendar view settings
|
||
calendarViewSettings: CalendarViewSettings;
|
||
// Pomodoro settings
|
||
pomodoroWorkDuration: number; // minutes
|
||
pomodoroShortBreakDuration: number; // minutes
|
||
pomodoroLongBreakDuration: number; // minutes
|
||
pomodoroLongBreakInterval: number; // after X pomodoros
|
||
pomodoroAutoStartBreaks: boolean;
|
||
pomodoroAutoStartWork: boolean;
|
||
pomodoroNotifications: boolean;
|
||
pomodoroSoundEnabled: boolean;
|
||
pomodoroSoundVolume: number; // 0-100
|
||
pomodoroStorageLocation: 'plugin' | 'daily-notes'; // where to store pomodoro history data
|
||
// Editor settings
|
||
enableTaskLinkOverlay: boolean;
|
||
enableInstantTaskConvert: boolean;
|
||
useDefaultsOnInstantConvert: boolean;
|
||
enableNaturalLanguageInput: boolean;
|
||
nlpDefaultToScheduled: boolean;
|
||
// Inline task conversion settings
|
||
inlineTaskConvertFolder: string; // Folder for inline task conversion, supports {{currentNotePath}}
|
||
// Performance settings
|
||
disableNoteIndexing: boolean;
|
||
// Customization settings
|
||
fieldMapping: FieldMapping;
|
||
customStatuses: StatusConfig[];
|
||
customPriorities: PriorityConfig[];
|
||
// Migration tracking
|
||
recurrenceMigrated?: boolean;
|
||
// Status bar settings
|
||
showTrackedTasksInStatusBar: boolean;
|
||
// Time tracking settings
|
||
autoStopTimeTrackingOnComplete: boolean;
|
||
autoStopTimeTrackingNotification: boolean;
|
||
// Project subtasks widget settings
|
||
showProjectSubtasks: boolean;
|
||
showExpandableSubtasks: boolean;
|
||
projectSubtasksPosition: 'top' | 'bottom';
|
||
// Overdue behavior settings
|
||
hideCompletedFromOverdue: boolean;
|
||
// ICS integration settings
|
||
icsIntegration: ICSIntegrationSettings;
|
||
// Saved filter views
|
||
savedViews: SavedView[];
|
||
// Notification settings
|
||
enableNotifications: boolean;
|
||
notificationType: 'in-app' | 'system';
|
||
// HTTP API settings
|
||
enableAPI: boolean;
|
||
apiPort: number;
|
||
apiAuthToken: string;
|
||
// Webhook settings
|
||
webhooks: WebhookConfig[];
|
||
}
|
||
|
||
export interface DefaultReminder {
|
||
id: string;
|
||
type: 'relative' | 'absolute';
|
||
// For relative reminders
|
||
relatedTo?: 'due' | 'scheduled';
|
||
offset?: number; // Amount in specified unit
|
||
unit?: 'minutes' | 'hours' | 'days';
|
||
direction?: 'before' | 'after';
|
||
// For absolute reminders
|
||
absoluteTime?: string; // Time in HH:MM format
|
||
absoluteDate?: string; // Date in YYYY-MM-DD format
|
||
description?: string;
|
||
}
|
||
|
||
export interface TaskCreationDefaults {
|
||
// Pre-fill options
|
||
defaultContexts: string; // Comma-separated list
|
||
defaultTags: string; // Comma-separated list
|
||
defaultProjects: string; // Comma-separated list of project links
|
||
useParentNoteAsProject: boolean; // Use the parent note as a project during instant conversion
|
||
defaultTimeEstimate: number; // minutes, 0 = no default
|
||
defaultRecurrence: 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||
// Date defaults
|
||
defaultDueDate: 'none' | 'today' | 'tomorrow' | 'next-week';
|
||
defaultScheduledDate: 'none' | 'today' | 'tomorrow' | 'next-week';
|
||
// Body template settings
|
||
bodyTemplate: string; // Path to template file for task body, empty = no template
|
||
useBodyTemplate: boolean; // Whether to use body template by default
|
||
// Reminder defaults
|
||
defaultReminders: DefaultReminder[];
|
||
}
|
||
|
||
export interface ICSIntegrationSettings {
|
||
// Default templates for creating content from ICS events
|
||
defaultNoteTemplate: string; // Path to template file for notes created from ICS events
|
||
// Default folders
|
||
defaultNoteFolder: string; // Folder for notes created from ICS events
|
||
}
|
||
|
||
export interface CalendarViewSettings {
|
||
// Default view
|
||
defaultView: 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay' | 'multiMonthYear' | 'timeGridCustom';
|
||
// Custom multi-day view settings
|
||
customDayCount: number; // Number of days to show in custom view (2-10)
|
||
// Time settings
|
||
slotDuration: '00:15:00' | '00:30:00' | '01:00:00'; // 15, 30, or 60 minutes
|
||
slotMinTime: string; // Start time (HH:MM:SS format)
|
||
slotMaxTime: string; // End time (HH:MM:SS format)
|
||
scrollTime: string; // Initial scroll position (HH:MM:SS format)
|
||
// Week settings
|
||
firstDay: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Sunday, 1 = Monday, etc.
|
||
// Display preferences
|
||
timeFormat: '12' | '24'; // 12-hour or 24-hour format
|
||
showWeekends: boolean;
|
||
// Default event type visibility
|
||
defaultShowScheduled: boolean;
|
||
defaultShowDue: boolean;
|
||
defaultShowDueWhenScheduled: boolean;
|
||
defaultShowTimeEntries: boolean;
|
||
defaultShowRecurring: boolean;
|
||
defaultShowICSEvents: boolean;
|
||
// Timeblocking settings
|
||
enableTimeblocking: boolean;
|
||
defaultShowTimeblocks: boolean;
|
||
// Calendar behavior
|
||
nowIndicator: boolean;
|
||
selectMirror: boolean;
|
||
weekNumbers: boolean;
|
||
// Today highlighting
|
||
showTodayHighlight: boolean;
|
||
}
|
||
|
||
// Default field mapping maintains backward compatibility
|
||
export const DEFAULT_FIELD_MAPPING: FieldMapping = {
|
||
title: 'title',
|
||
status: 'status',
|
||
priority: 'priority',
|
||
due: 'due',
|
||
scheduled: 'scheduled',
|
||
contexts: 'contexts',
|
||
projects: 'projects',
|
||
timeEstimate: 'timeEstimate',
|
||
completedDate: 'completedDate',
|
||
dateCreated: 'dateCreated',
|
||
dateModified: 'dateModified',
|
||
recurrence: 'recurrence',
|
||
archiveTag: 'archived',
|
||
timeEntries: 'timeEntries',
|
||
completeInstances: 'complete_instances',
|
||
pomodoros: 'pomodoros',
|
||
icsEventId: 'icsEventId',
|
||
icsEventTag: 'ics_event',
|
||
reminders: 'reminders'
|
||
};
|
||
|
||
// Default status configuration matches current hardcoded behavior
|
||
export const DEFAULT_STATUSES: StatusConfig[] = [
|
||
{
|
||
id: 'none',
|
||
value: 'none',
|
||
label: 'None',
|
||
color: '#cccccc',
|
||
isCompleted: false,
|
||
order: 0
|
||
},
|
||
{
|
||
id: 'open',
|
||
value: 'open',
|
||
label: 'Open',
|
||
color: '#808080',
|
||
isCompleted: false,
|
||
order: 1
|
||
},
|
||
{
|
||
id: 'in-progress',
|
||
value: 'in-progress',
|
||
label: 'In progress',
|
||
color: '#0066cc',
|
||
isCompleted: false,
|
||
order: 2
|
||
},
|
||
{
|
||
id: 'done',
|
||
value: 'done',
|
||
label: 'Done',
|
||
color: '#00aa00',
|
||
isCompleted: true,
|
||
order: 3
|
||
}
|
||
];
|
||
|
||
// Default priority configuration matches current hardcoded behavior
|
||
export const DEFAULT_PRIORITIES: PriorityConfig[] = [
|
||
{
|
||
id: 'none',
|
||
value: 'none',
|
||
label: 'None',
|
||
color: '#cccccc',
|
||
weight: 0
|
||
},
|
||
{
|
||
id: 'low',
|
||
value: 'low',
|
||
label: 'Low',
|
||
color: '#00aa00',
|
||
weight: 1
|
||
},
|
||
{
|
||
id: 'normal',
|
||
value: 'normal',
|
||
label: 'Normal',
|
||
color: '#ffaa00',
|
||
weight: 2
|
||
},
|
||
{
|
||
id: 'high',
|
||
value: 'high',
|
||
label: 'High',
|
||
color: '#ff0000',
|
||
weight: 3
|
||
}
|
||
];
|
||
|
||
export const DEFAULT_TASK_CREATION_DEFAULTS: TaskCreationDefaults = {
|
||
defaultContexts: '',
|
||
defaultTags: '',
|
||
defaultProjects: '',
|
||
useParentNoteAsProject: false,
|
||
defaultTimeEstimate: 0,
|
||
defaultRecurrence: 'none',
|
||
defaultDueDate: 'none',
|
||
defaultScheduledDate: 'today',
|
||
bodyTemplate: '',
|
||
useBodyTemplate: false,
|
||
defaultReminders: []
|
||
};
|
||
|
||
export const DEFAULT_CALENDAR_VIEW_SETTINGS: CalendarViewSettings = {
|
||
// Default view
|
||
defaultView: 'dayGridMonth',
|
||
// Custom multi-day view settings
|
||
customDayCount: 3, // Default to 3 days as requested in issue #282
|
||
// Time settings
|
||
slotDuration: '00:30:00', // 30-minute slots
|
||
slotMinTime: '00:00:00', // Start at midnight
|
||
slotMaxTime: '24:00:00', // End at midnight next day
|
||
scrollTime: '08:00:00', // Scroll to 8 AM
|
||
// Week settings
|
||
firstDay: 1, // Monday
|
||
// Display preferences
|
||
timeFormat: '24', // 24-hour format
|
||
showWeekends: true,
|
||
// Default event type visibility
|
||
defaultShowScheduled: true,
|
||
defaultShowDue: true,
|
||
defaultShowDueWhenScheduled: true,
|
||
defaultShowTimeEntries: false,
|
||
defaultShowRecurring: true,
|
||
defaultShowICSEvents: true,
|
||
// Timeblocking settings
|
||
enableTimeblocking: false, // Disabled by default - toggleable feature
|
||
defaultShowTimeblocks: true,
|
||
// Calendar behavior
|
||
nowIndicator: true,
|
||
selectMirror: true,
|
||
weekNumbers: false,
|
||
// Today highlighting
|
||
showTodayHighlight: true
|
||
};
|
||
|
||
export const DEFAULT_SETTINGS: TaskNotesSettings = {
|
||
tasksFolder: 'TaskNotes/Tasks',
|
||
taskTag: 'task',
|
||
taskIdentificationMethod: 'tag', // Default to tag-based identification
|
||
taskPropertyName: '',
|
||
taskPropertyValue: '',
|
||
excludedFolders: '', // Default to no excluded folders
|
||
defaultTaskPriority: 'normal',
|
||
defaultTaskStatus: 'open',
|
||
taskOrgFiltersCollapsed: false, // Default to expanded
|
||
// Task filename defaults
|
||
taskFilenameFormat: 'zettel', // Keep existing behavior as default
|
||
storeTitleInFilename: true,
|
||
customFilenameTemplate: '{title}', // Simple title template
|
||
// Task creation defaults
|
||
taskCreationDefaults: DEFAULT_TASK_CREATION_DEFAULTS,
|
||
// Calendar view defaults
|
||
calendarViewSettings: DEFAULT_CALENDAR_VIEW_SETTINGS,
|
||
// Pomodoro defaults
|
||
pomodoroWorkDuration: 25,
|
||
pomodoroShortBreakDuration: 5,
|
||
pomodoroLongBreakDuration: 15,
|
||
pomodoroLongBreakInterval: 4,
|
||
pomodoroAutoStartBreaks: true,
|
||
pomodoroAutoStartWork: false,
|
||
pomodoroNotifications: true,
|
||
pomodoroSoundEnabled: true,
|
||
pomodoroSoundVolume: 50,
|
||
pomodoroStorageLocation: 'plugin',
|
||
// Editor defaults
|
||
enableTaskLinkOverlay: true,
|
||
enableInstantTaskConvert: true,
|
||
useDefaultsOnInstantConvert: true,
|
||
enableNaturalLanguageInput: true,
|
||
nlpDefaultToScheduled: true,
|
||
// Inline task conversion defaults
|
||
inlineTaskConvertFolder: '{{currentNotePath}}',
|
||
// Performance defaults
|
||
disableNoteIndexing: false,
|
||
// Customization defaults
|
||
fieldMapping: DEFAULT_FIELD_MAPPING,
|
||
customStatuses: DEFAULT_STATUSES,
|
||
customPriorities: DEFAULT_PRIORITIES,
|
||
// Migration defaults
|
||
recurrenceMigrated: false,
|
||
// Status bar defaults
|
||
showTrackedTasksInStatusBar: false,
|
||
// Time tracking defaults
|
||
autoStopTimeTrackingOnComplete: true,
|
||
autoStopTimeTrackingNotification: false,
|
||
// Project subtasks widget defaults
|
||
showProjectSubtasks: true,
|
||
showExpandableSubtasks: true,
|
||
projectSubtasksPosition: 'bottom',
|
||
// Overdue behavior defaults
|
||
hideCompletedFromOverdue: true,
|
||
// ICS integration defaults
|
||
icsIntegration: {
|
||
defaultNoteTemplate: '',
|
||
defaultNoteFolder: ''
|
||
},
|
||
// Saved filter views defaults
|
||
savedViews: [],
|
||
// Notification defaults
|
||
enableNotifications: true,
|
||
notificationType: 'system',
|
||
// HTTP API defaults
|
||
enableAPI: false,
|
||
apiPort: 8080,
|
||
apiAuthToken: '',
|
||
// Webhook defaults
|
||
webhooks: []
|
||
};
|
||
|
||
/**
|
||
* Converts DefaultReminder objects to Reminder objects that can be used in tasks.
|
||
* This function handles the conversion of user-configured default reminders into
|
||
* the format expected by the task reminder system.
|
||
*/
|
||
export function convertDefaultRemindersToReminders(
|
||
defaultReminders: DefaultReminder[],
|
||
task?: TaskInfo
|
||
): Reminder[] {
|
||
return defaultReminders.map(defaultReminder => {
|
||
const reminder: Reminder = {
|
||
id: `rem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||
type: defaultReminder.type,
|
||
description: defaultReminder.description
|
||
};
|
||
|
||
if (defaultReminder.type === 'relative') {
|
||
// For relative reminders, validate that the anchor date will be available
|
||
if (defaultReminder.relatedTo && defaultReminder.offset && defaultReminder.unit && defaultReminder.direction) {
|
||
// Convert offset to ISO 8601 duration format
|
||
let duration = 'PT';
|
||
if (defaultReminder.unit === 'days') {
|
||
duration = `P${defaultReminder.offset}D`;
|
||
} else if (defaultReminder.unit === 'hours') {
|
||
duration = `PT${defaultReminder.offset}H`;
|
||
} else {
|
||
duration = `PT${defaultReminder.offset}M`;
|
||
}
|
||
|
||
// Add negative sign for "before"
|
||
if (defaultReminder.direction === 'before') {
|
||
duration = '-' + duration;
|
||
}
|
||
|
||
reminder.relatedTo = defaultReminder.relatedTo;
|
||
reminder.offset = duration;
|
||
}
|
||
} else if (defaultReminder.type === 'absolute') {
|
||
// For absolute reminders, convert date and time to ISO string
|
||
if (defaultReminder.absoluteDate && defaultReminder.absoluteTime) {
|
||
reminder.absoluteTime = `${defaultReminder.absoluteDate}T${defaultReminder.absoluteTime}:00`;
|
||
}
|
||
}
|
||
|
||
return reminder;
|
||
}).filter(reminder => {
|
||
// Filter out invalid reminders
|
||
if (reminder.type === 'relative') {
|
||
return reminder.relatedTo && reminder.offset;
|
||
} else {
|
||
return reminder.absoluteTime;
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
export class TaskNotesSettingTab extends PluginSettingTab {
|
||
plugin: TaskNotesPlugin;
|
||
private activeTab = 'task-defaults';
|
||
private tabContents: Record<string, HTMLElement> = {};
|
||
private selectedDefaultProjectFiles: TAbstractFile[] = [];
|
||
|
||
constructor(app: App, plugin: TaskNotesPlugin) {
|
||
super(app, plugin);
|
||
this.plugin = plugin;
|
||
}
|
||
|
||
display(): void {
|
||
const { containerEl } = this;
|
||
containerEl.empty();
|
||
containerEl.addClass('tasknotes-settings');
|
||
containerEl.addClass('tasknotes-plugin');
|
||
containerEl.addClass('settings-view');
|
||
|
||
// Create tab navigation
|
||
const tabNav = containerEl.createDiv('settings-tab-nav settings-view__tab-nav');
|
||
|
||
const allTabs = [
|
||
{ id: 'task-defaults', name: 'Task defaults' },
|
||
{ id: 'general', name: 'Inline tasks' },
|
||
{ id: 'calendar', name: 'Calendar' },
|
||
{ id: 'field-mapping', name: 'Field mapping' },
|
||
{ id: 'statuses', name: 'Statuses' },
|
||
{ id: 'priorities', name: 'Priorities' },
|
||
{ id: 'pomodoro', name: 'Pomodoro' },
|
||
{ id: 'notifications', name: 'Notifications' },
|
||
{ id: 'api', name: 'HTTP API' },
|
||
{ id: 'misc', name: 'Misc' }
|
||
];
|
||
|
||
// Filter out API tab on mobile
|
||
const tabs = Platform.isMobile ? allTabs.filter(tab => tab.id !== 'api') : allTabs;
|
||
|
||
// Reset active tab if it's 'api' on mobile
|
||
if (Platform.isMobile && this.activeTab === 'api') {
|
||
this.activeTab = 'general';
|
||
}
|
||
|
||
tabs.forEach(tab => {
|
||
const isActive = this.activeTab === tab.id;
|
||
const tabButton = tabNav.createEl('button', {
|
||
text: tab.name,
|
||
cls: isActive ? 'settings-tab-button settings-view__tab-button active settings-view__tab-button--active' : 'settings-tab-button settings-view__tab-button',
|
||
attr: {
|
||
'role': 'tab',
|
||
'aria-selected': isActive.toString(),
|
||
'aria-controls': `settings-tab-${tab.id}`,
|
||
'id': `tab-button-${tab.id}`,
|
||
'tabindex': isActive ? '0' : '-1'
|
||
}
|
||
});
|
||
|
||
tabButton.addEventListener('click', () => {
|
||
this.switchTab(tab.id);
|
||
});
|
||
|
||
tabButton.addEventListener('keydown', (e) => {
|
||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||
e.preventDefault();
|
||
const currentIndex = tabs.findIndex(t => t.id === tab.id);
|
||
const nextIndex = e.key === 'ArrowRight'
|
||
? (currentIndex + 1) % tabs.length
|
||
: (currentIndex - 1 + tabs.length) % tabs.length;
|
||
const nextTabId = tabs[nextIndex].id;
|
||
this.switchTab(nextTabId);
|
||
// Focus will be set in switchTab
|
||
}
|
||
});
|
||
});
|
||
|
||
// Create tab content containers
|
||
const tabContentsEl = containerEl.createDiv('settings-tab-contents settings-view__tab-contents');
|
||
|
||
// Create all tab content containers
|
||
tabs.forEach(tab => {
|
||
const tabContent = tabContentsEl.createDiv('settings-tab-content settings-view__tab-content');
|
||
tabContent.setAttribute('role', 'tabpanel');
|
||
tabContent.setAttribute('id', `settings-tab-${tab.id}`);
|
||
tabContent.setAttribute('aria-labelledby', `tab-button-${tab.id}`);
|
||
if (this.activeTab === tab.id) {
|
||
tabContent.addClass('active');
|
||
tabContent.addClass('settings-view__tab-content--active');
|
||
}
|
||
this.tabContents[tab.id] = tabContent;
|
||
});
|
||
|
||
this.renderActiveTab();
|
||
}
|
||
|
||
private switchTab(tabId: string): void {
|
||
this.activeTab = tabId;
|
||
this.display(); // Re-render the entire settings tab
|
||
|
||
// Focus the newly active tab button
|
||
window.setTimeout(() => {
|
||
const activeTabButton = this.containerEl.querySelector(`#tab-button-${tabId}`) as HTMLElement;
|
||
if (activeTabButton) {
|
||
activeTabButton.focus();
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
private renderActiveTab(): void {
|
||
// Clear current tab content
|
||
Object.values(this.tabContents).forEach(content => content.empty());
|
||
|
||
switch (this.activeTab) {
|
||
case 'general':
|
||
this.renderGeneralTab();
|
||
break;
|
||
case 'task-defaults':
|
||
this.renderTaskDefaultsTab();
|
||
break;
|
||
case 'calendar':
|
||
this.renderCalendarTab();
|
||
break;
|
||
case 'field-mapping':
|
||
this.renderFieldMappingTab();
|
||
break;
|
||
case 'statuses':
|
||
this.renderStatusesTab();
|
||
break;
|
||
case 'priorities':
|
||
this.renderPrioritiesTab();
|
||
break;
|
||
case 'pomodoro':
|
||
this.renderPomodoroTab();
|
||
break;
|
||
case 'notifications':
|
||
this.renderNotificationsTab();
|
||
break;
|
||
case 'api':
|
||
this.renderAPITab();
|
||
break;
|
||
case 'misc':
|
||
this.renderMiscTab();
|
||
break;
|
||
}
|
||
}
|
||
|
||
private renderGeneralTab(): void {
|
||
const container = this.tabContents['general'];
|
||
|
||
// Inline task settings
|
||
new Setting(container).setName('Inline tasks').setHeading();
|
||
|
||
container.createEl('p', {
|
||
text: 'Configure how TaskNotes integrates with your editor and existing Markdown tasks.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Task link overlay')
|
||
.setDesc('Replace wikilinks to task files with interactive task cards in both live preview and reading modes')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Enable task link overlay in live preview and reading modes');
|
||
return toggle
|
||
.setValue(this.plugin.settings.enableTaskLinkOverlay)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableTaskLinkOverlay = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Instant task convert')
|
||
.setDesc('Show a convert button next to checkbox tasks for instant conversion to TaskNotes')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Enable instant task conversion buttons');
|
||
return toggle
|
||
.setValue(this.plugin.settings.enableInstantTaskConvert)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableInstantTaskConvert = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Use task defaults on instant convert')
|
||
.setDesc('Apply your configured task creation defaults when converting checkbox tasks to TaskNotes (contexts, tags, dates, folder, etc.)')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Apply task creation defaults during instant conversion');
|
||
return toggle
|
||
.setValue(this.plugin.settings.useDefaultsOnInstantConvert)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.useDefaultsOnInstantConvert = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Inline task folder')
|
||
.setDesc('Folder for converted inline tasks. Use {{currentNotePath}} to place tasks in the same folder as the note.')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Folder for inline task conversion');
|
||
return text
|
||
.setPlaceholder('{{currentNotePath}}')
|
||
.setValue(this.plugin.settings.inlineTaskConvertFolder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.inlineTaskConvertFolder = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
|
||
// Help section
|
||
const helpContainer = container.createDiv('settings-help-section');
|
||
helpContainer.createEl('h4', { text: 'How inline task features work:' });
|
||
const helpList = helpContainer.createEl('ul');
|
||
helpList.createEl('li', { text: 'Task link overlay: When you link to a task file like [[My Task]], it shows an interactive task card instead of a plain link' });
|
||
helpList.createEl('li', { text: 'Instant task convert: Shows a "Convert to TaskNote" button next to standard Markdown checkboxes like - [ ] My task' });
|
||
|
||
helpContainer.createEl('p', {
|
||
text: 'These features help bridge regular Markdown tasks with the full TaskNotes system.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
}
|
||
|
||
private renderTaskDefaultsTab(): void {
|
||
const container = this.tabContents['task-defaults'];
|
||
|
||
// Task organization section
|
||
new Setting(container).setName('Task organization').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Default tasks folder')
|
||
.setDesc('Default folder for new tasks (tasks are identified by tag, not folder)')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Default tasks folder path');
|
||
return text
|
||
.setPlaceholder('TaskNotes/Tasks')
|
||
.setValue(this.plugin.settings.tasksFolder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.tasksFolder = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Identify tasks by')
|
||
.setDesc('Choose whether to identify tasks by tag or by a frontmatter property')
|
||
.addDropdown(dropdown => {
|
||
dropdown
|
||
.addOption('tag', 'Tag')
|
||
.addOption('property', 'Property')
|
||
.setValue(this.plugin.settings.taskIdentificationMethod)
|
||
.onChange(async (value: 'tag' | 'property') => {
|
||
this.plugin.settings.taskIdentificationMethod = value;
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab(); // Re-render to show/hide conditional fields
|
||
});
|
||
});
|
||
|
||
if (this.plugin.settings.taskIdentificationMethod === 'tag') {
|
||
new Setting(container)
|
||
.setName('Task tag')
|
||
.setDesc('Tag that identifies notes as tasks (without #)')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Task identification tag');
|
||
return text
|
||
.setPlaceholder('task')
|
||
.setValue(this.plugin.settings.taskTag)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskTag = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
} else { // Property-based identification
|
||
new Setting(container)
|
||
.setName('Task property name')
|
||
.setDesc('The frontmatter property name (e.g., "category")')
|
||
.addText(text => text
|
||
.setPlaceholder('category')
|
||
.setValue(this.plugin.settings.taskPropertyName)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskPropertyName = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Task property value')
|
||
.setDesc('The value that identifies a task (e.g., "[[Tasks]]")')
|
||
.addText(text => text
|
||
.setPlaceholder('[[Tasks]]')
|
||
.setValue(this.plugin.settings.taskPropertyValue)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskPropertyValue = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
}
|
||
|
||
new Setting(container)
|
||
.setName('Excluded folders')
|
||
.setDesc('Comma-separated list of folder paths to exclude from Notes tab')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Comma-separated list of folders to exclude');
|
||
return text
|
||
.setPlaceholder('Templates,Archive')
|
||
.setValue(this.plugin.settings.excludedFolders)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.excludedFolders = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Task filename settings
|
||
new Setting(container).setName('Task filenames').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Store title exclusively in filename')
|
||
.setDesc("When disabled, the task's title will be stored in the note frontmatter. You will be able to define custom templates for the filename of the task, but these will not be updated by the TaskNotes plugin after task creation. Moving the title into the frontmatter is a significant storage change and care must be taken when mixing tasks with their titles in the filename, and those with their titles in the frontmatter. Toggling this option will not affect existing tasks; it will affect tasks on creation and edit.")
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Store title exclusively in filename');
|
||
return toggle
|
||
.setValue(this.plugin.settings.storeTitleInFilename)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.storeTitleInFilename = value;
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab(); // Re-render to show/hide other options
|
||
});
|
||
});
|
||
|
||
if (!this.plugin.settings.storeTitleInFilename) {
|
||
new Setting(container)
|
||
.setName('Filename format')
|
||
.setDesc('How task filenames should be generated')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Task filename generation format');
|
||
return dropdown
|
||
.addOption('title', 'Task title')
|
||
.addOption('zettel', 'Zettelkasten format (YYMMDD + base36 seconds since midnight)')
|
||
.addOption('timestamp', 'Full timestamp (YYYY-MM-DD-HHMMSS)')
|
||
.addOption('custom', 'Custom template')
|
||
.setValue(this.plugin.settings.taskFilenameFormat)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.taskFilenameFormat = value;
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab(); // Re-render to update visibility
|
||
});
|
||
});
|
||
|
||
if (this.plugin.settings.taskFilenameFormat === 'custom') {
|
||
new Setting(container)
|
||
.setName('Custom filename template')
|
||
.setDesc('Template for custom filenames. Available variables: {title}, {titleLower}, {titleUpper}, {titleSnake}, {titleKebab}, {titleCamel}, {titlePascal}, {date}, {shortDate}, {time}, {time12}, {time24}, {timestamp}, {dateTime}, {year}, {month}, {monthName}, {monthNameShort}, {day}, {dayName}, {dayNameShort}, {hour}, {hour12}, {minute}, {second}, {milliseconds}, {ms}, {ampm}, {week}, {quarter}, {unix}, {unixMs}, {timezone}, {timezoneShort}, {utcOffset}, {utcOffsetShort}, {utcZ}, {zettel}, {nano}, {priority}, {priorityShort}, {status}, {statusShort}, {dueDate}, {scheduledDate}')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Custom filename template with variables');
|
||
return text
|
||
.setPlaceholder('{date}-{title}-{dueDate}')
|
||
.setValue(this.plugin.settings.customFilenameTemplate)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.customFilenameTemplate = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
container.createEl('p', {
|
||
text: 'Note: {dueDate} and {scheduledDate} are in YYYY-MM-DD format and will be empty if not set.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
}
|
||
}
|
||
|
||
// Basic defaults section
|
||
new Setting(container).setName('Basic defaults').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Enable natural language task input')
|
||
.setDesc('Show a smart input field in task creation modal that can parse natural language like "Buy groceries tomorrow 3pm high priority @home #errands"')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Enable natural language task input');
|
||
return toggle
|
||
.setValue(this.plugin.settings.enableNaturalLanguageInput)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableNaturalLanguageInput = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default date type for natural language input')
|
||
.setDesc('When dates are mentioned without "due" or "scheduled" keywords (e.g., "task friday"), default to scheduled date. When disabled, defaults to due date.')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Default to scheduled date for natural language input');
|
||
return toggle
|
||
.setValue(this.plugin.settings.nlpDefaultToScheduled)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.nlpDefaultToScheduled = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default task status')
|
||
.setDesc('Default status for new tasks')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Default status for new tasks');
|
||
// Populate with custom statuses
|
||
this.plugin.settings.customStatuses.forEach(status => {
|
||
dropdown.addOption(status.value, status.label);
|
||
});
|
||
return dropdown
|
||
.setValue(this.plugin.settings.defaultTaskStatus)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.defaultTaskStatus = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default task priority')
|
||
.setDesc('Default priority for new tasks')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Default priority for new tasks');
|
||
// Populate with custom priorities
|
||
this.plugin.settings.customPriorities.forEach(priority => {
|
||
dropdown.addOption(priority.value, priority.label);
|
||
});
|
||
return dropdown
|
||
.setValue(this.plugin.settings.defaultTaskPriority)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.defaultTaskPriority = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default contexts')
|
||
.setDesc('Default contexts for new tasks (comma-separated)')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Default contexts for new tasks');
|
||
return text
|
||
.setPlaceholder('work, personal')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.defaultContexts)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskCreationDefaults.defaultContexts = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
|
||
new Setting(container)
|
||
.setName('Default tags')
|
||
.setDesc('Default tags for new tasks (comma-separated)')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Default tags for new tasks');
|
||
return text
|
||
.setPlaceholder('project, urgent')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.defaultTags)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskCreationDefaults.defaultTags = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Default projects setting
|
||
const projectSetting = new Setting(container)
|
||
.setName('Default projects')
|
||
.setDesc('Default projects for new tasks');
|
||
|
||
// Initialize default project files from settings
|
||
this.initializeDefaultProjectsFromSettings();
|
||
|
||
// Create projects display area
|
||
const projectsContainer = projectSetting.settingEl.createDiv('default-projects-container');
|
||
const projectsList = projectsContainer.createDiv('default-projects-list');
|
||
|
||
// Add project button
|
||
const addProjectBtn = projectsContainer.createEl('button', {
|
||
cls: 'default-project-add-btn',
|
||
text: '+ Add project'
|
||
});
|
||
addProjectBtn.addEventListener('click', () => {
|
||
new ProjectSelectModal(this.app, this.plugin, (file) => {
|
||
this.addDefaultProject(file);
|
||
this.renderDefaultProjectsList(projectsList);
|
||
this.updateDefaultProjectsInSettings();
|
||
}).open();
|
||
});
|
||
|
||
// Initial render
|
||
this.renderDefaultProjectsList(projectsList);
|
||
|
||
new Setting(container)
|
||
.setName('Use parent note as project')
|
||
.setDesc('During instant task conversion, automatically add the parent note as a project')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Use parent note as project for instant conversion');
|
||
return toggle
|
||
.setValue(this.plugin.settings.taskCreationDefaults.useParentNoteAsProject)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskCreationDefaults.useParentNoteAsProject = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default time estimate')
|
||
.setDesc('Default time estimate for new tasks in minutes (0 = no default)')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Default time estimate in minutes');
|
||
return text
|
||
.setPlaceholder('0')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.defaultTimeEstimate.toString())
|
||
.onChange(async (value) => {
|
||
const num = parseInt(value) || 0;
|
||
this.plugin.settings.taskCreationDefaults.defaultTimeEstimate = num;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default recurrence')
|
||
.setDesc('Default recurrence pattern for new tasks')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Default recurrence pattern');
|
||
return dropdown
|
||
.addOption('none', 'None')
|
||
.addOption('daily', 'Daily')
|
||
.addOption('weekly', 'Weekly')
|
||
.addOption('monthly', 'Monthly')
|
||
.addOption('yearly', 'Yearly')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.defaultRecurrence)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.taskCreationDefaults.defaultRecurrence = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Date defaults section
|
||
new Setting(container).setName('Date defaults').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Default due date')
|
||
.setDesc('Default due date for new tasks')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Default due date for new tasks');
|
||
return dropdown
|
||
.addOption('none', 'None')
|
||
.addOption('today', 'Today')
|
||
.addOption('tomorrow', 'Tomorrow')
|
||
.addOption('next-week', 'Next week')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.defaultDueDate)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.taskCreationDefaults.defaultDueDate = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Default scheduled date')
|
||
.setDesc('Default scheduled date for new tasks')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Default scheduled date for new tasks');
|
||
return dropdown
|
||
.addOption('none', 'None')
|
||
.addOption('today', 'Today')
|
||
.addOption('tomorrow', 'Tomorrow')
|
||
.addOption('next-week', 'Next week')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.defaultScheduledDate)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.taskCreationDefaults.defaultScheduledDate = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Body template section
|
||
new Setting(container).setName('Body template').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Use body template')
|
||
.setDesc('Pre-fill task details with content from a template file')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Enable body template for new tasks');
|
||
return toggle
|
||
.setValue(this.plugin.settings.taskCreationDefaults.useBodyTemplate)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskCreationDefaults.useBodyTemplate = value;
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab(); // Re-render to show/hide template path
|
||
});
|
||
});
|
||
|
||
if (this.plugin.settings.taskCreationDefaults.useBodyTemplate) {
|
||
new Setting(container)
|
||
.setName('Body template file')
|
||
.setDesc('Path to template file for task body content. Supports template variables like {{title}}, {{date}}, {{time}}, {{priority}}, {{status}}, etc.')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Path to body template file');
|
||
return text
|
||
.setPlaceholder('Templates/Task Template.md')
|
||
.setValue(this.plugin.settings.taskCreationDefaults.bodyTemplate)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.taskCreationDefaults.bodyTemplate = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Help section
|
||
const helpContainer = container.createDiv('settings-help-section');
|
||
helpContainer.createEl('h4', { text: 'Template variables:' });
|
||
const helpList = helpContainer.createEl('ul');
|
||
helpList.createEl('li', { text: '{{title}} - Task title' });
|
||
helpList.createEl('li', { text: '{{details}} - User-provided details from modal' });
|
||
helpList.createEl('li', { text: '{{date}} - Current date (YYYY-MM-DD)' });
|
||
helpList.createEl('li', { text: '{{time}} - Current time (HH:MM)' });
|
||
helpList.createEl('li', { text: '{{priority}} - Task priority' });
|
||
helpList.createEl('li', { text: '{{status}} - Task status' });
|
||
helpList.createEl('li', { text: '{{contexts}} - Task contexts' });
|
||
helpList.createEl('li', { text: '{{tags}} - Task tags' });
|
||
helpList.createEl('li', { text: '{{timeEstimate}} - Time estimate in minutes' });
|
||
helpList.createEl('li', { text: '{{dueDate}} - Task due date' });
|
||
helpList.createEl('li', { text: '{{scheduledDate}} - Task scheduled date' });
|
||
helpList.createEl('li', { text: '{{parentNote}} - Parent note as a properly formatted markdown link' });
|
||
|
||
helpContainer.createEl('p', {
|
||
text: 'Template is applied when the task is created with all final values from the form. Use {{details}} to include user content from the Details field.\n{{parentNote}} will resolve to a quoted markdown link (e.g., "[[Note Name]]") for the note where the task was created. For project organization, use it as a YAML list item: "project:\\n - {{parentNote}}". Variables use the same format as daily note templates.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Reminder defaults section
|
||
new Setting(container).setName('Reminder defaults').setHeading();
|
||
|
||
const reminderSection = container.createDiv('reminder-defaults-section');
|
||
reminderSection.createEl('p', {
|
||
text: 'Configure default reminders that will be automatically added to new tasks. These can be relative to due or scheduled dates.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Current default reminders list
|
||
const remindersList = reminderSection.createDiv('reminder-defaults-list');
|
||
this.renderDefaultRemindersList(remindersList);
|
||
|
||
// Add reminder form
|
||
this.renderAddDefaultReminderForm(reminderSection);
|
||
|
||
|
||
}
|
||
|
||
private renderCalendarTab(): void {
|
||
const container = this.tabContents['calendar'];
|
||
|
||
// Calendar view section
|
||
new Setting(container).setName('Calendar view settings').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Default view')
|
||
.setDesc('Initial view when opening the Advanced Calendar')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Default calendar view');
|
||
return dropdown
|
||
.addOption('dayGridMonth', 'Month')
|
||
.addOption('timeGridWeek', 'Week')
|
||
.addOption('timeGridDay', 'Day')
|
||
.addOption('timeGridCustom', 'Custom Days')
|
||
.addOption('multiMonthYear', 'Year')
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultView)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.calendarViewSettings.defaultView = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Custom view day count')
|
||
.setDesc('Number of days to show in the custom view (2-10 days)')
|
||
.addSlider(slider => {
|
||
slider.sliderEl.setAttribute('aria-label', 'Number of days in custom view');
|
||
return slider
|
||
.setLimits(2, 10, 1)
|
||
.setValue(this.plugin.settings.calendarViewSettings.customDayCount)
|
||
.setDynamicTooltip()
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.customDayCount = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('First day of week')
|
||
.setDesc('Which day should be the first column in week and month views')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'First day of week');
|
||
return dropdown
|
||
.addOption('0', 'Sunday')
|
||
.addOption('1', 'Monday')
|
||
.addOption('2', 'Tuesday')
|
||
.addOption('3', 'Wednesday')
|
||
.addOption('4', 'Thursday')
|
||
.addOption('5', 'Friday')
|
||
.addOption('6', 'Saturday')
|
||
.setValue(this.plugin.settings.calendarViewSettings.firstDay.toString())
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.calendarViewSettings.firstDay = parseInt(value) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show weekends')
|
||
.setDesc('Display Saturday and Sunday in calendar views')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show weekends in calendar');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.showWeekends)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.showWeekends = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show week numbers')
|
||
.setDesc('Display week numbers on the left side of calendar views')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show week numbers');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.weekNumbers)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.weekNumbers = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Time settings section
|
||
new Setting(container).setName('Time settings').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Time format')
|
||
.setDesc('Display times in 12-hour or 24-hour format')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Time format');
|
||
return dropdown
|
||
.addOption('12', '12-hour (9:00 AM)')
|
||
.addOption('24', '24-hour (09:00)')
|
||
.setValue(this.plugin.settings.calendarViewSettings.timeFormat)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.calendarViewSettings.timeFormat = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Time slot duration')
|
||
.setDesc('Duration of each time slot in week and day views')
|
||
.addDropdown(dropdown => {
|
||
dropdown.selectEl.setAttribute('aria-label', 'Time slot duration');
|
||
return dropdown
|
||
.addOption('00:15:00', '15 minutes')
|
||
.addOption('00:30:00', '30 minutes')
|
||
.addOption('01:00:00', '1 hour')
|
||
.setValue(this.plugin.settings.calendarViewSettings.slotDuration)
|
||
.onChange(async (value: any) => {
|
||
this.plugin.settings.calendarViewSettings.slotDuration = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Day start time')
|
||
.setDesc('First time slot to display in day/week views')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Day start time (HH:MM:SS format)');
|
||
return text
|
||
.setPlaceholder('00:00:00')
|
||
.setValue(this.plugin.settings.calendarViewSettings.slotMinTime)
|
||
.onChange(async (value) => {
|
||
// Validate time format
|
||
if (!/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/.test(value)) {
|
||
return; // Invalid format, don't save
|
||
}
|
||
this.plugin.settings.calendarViewSettings.slotMinTime = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Day end time')
|
||
.setDesc('Last time slot to display in day/week views')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Day end time (HH:MM:SS format)');
|
||
return text
|
||
.setPlaceholder('24:00:00')
|
||
.setValue(this.plugin.settings.calendarViewSettings.slotMaxTime)
|
||
.onChange(async (value) => {
|
||
// Validate time format
|
||
if (!/^([0-1]?[0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9]$/.test(value)) {
|
||
return; // Invalid format, don't save
|
||
}
|
||
this.plugin.settings.calendarViewSettings.slotMaxTime = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Initial scroll time')
|
||
.setDesc('Time to scroll to when opening day/week views')
|
||
.addText(text => {
|
||
text.inputEl.setAttribute('aria-label', 'Initial scroll time (HH:MM:SS format)');
|
||
return text
|
||
.setPlaceholder('08:00:00')
|
||
.setValue(this.plugin.settings.calendarViewSettings.scrollTime)
|
||
.onChange(async (value) => {
|
||
// Validate time format
|
||
if (!/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/.test(value)) {
|
||
return; // Invalid format, don't save
|
||
}
|
||
this.plugin.settings.calendarViewSettings.scrollTime = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Event visibility section
|
||
new Setting(container).setName('Default event visibility').setHeading();
|
||
|
||
container.createEl('p', {
|
||
text: 'Configure which event types are visible by default when opening the Advanced Calendar. Users can still toggle these on/off in the calendar view.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show scheduled tasks')
|
||
.setDesc('Display tasks with scheduled dates by default')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show scheduled tasks by default');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowScheduled)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowScheduled = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show due dates')
|
||
.setDesc('Display task due dates by default')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show due dates by default');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowDue)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowDue = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show due dates when scheduled')
|
||
.setDesc('Display due dates even for tasks that already have scheduled dates')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show due dates when scheduled dates exist');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowDueWhenScheduled)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowDueWhenScheduled = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show time entries')
|
||
.setDesc('Display completed time tracking entries by default')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show time entries by default');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowTimeEntries)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowTimeEntries = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show recurring tasks')
|
||
.setDesc('Display recurring task instances by default')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show recurring tasks by default');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowRecurring)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowRecurring = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show ICS events')
|
||
.setDesc('Display events from ICS subscriptions by default')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show ICS events by default');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowICSEvents)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowICSEvents = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// Timeblocking section
|
||
new Setting(container).setName('Timeblocking').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Enable timeblocking')
|
||
.setDesc('Enable timeblock functionality for lightweight scheduling in daily notes')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Enable timeblocking feature');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.enableTimeblocking)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.enableTimeblocking = value;
|
||
await this.plugin.saveSettings();
|
||
// Refresh calendar views to show/hide timeblock functionality
|
||
this.plugin.emitter.trigger('timeblocking-toggled', value);
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Show timeblocks')
|
||
.setDesc('Display timeblocks from daily notes by default')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show timeblocks by default');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.defaultShowTimeblocks)
|
||
.setDisabled(!this.plugin.settings.calendarViewSettings.enableTimeblocking)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.defaultShowTimeblocks = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
container.createEl('p', {
|
||
text: 'Timeblocks are defined in daily note frontmatter using the "timeblocks" field. Each timeblock can have a title, start time, end time, and optional attachments as markdown links to tasks or notes.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
container.createEl('p', {
|
||
text: '💡 Tip: In the calendar view, hold Shift + drag to create timeblocks • Drag to move • Resize edges to adjust duration',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Calendar behavior section
|
||
new Setting(container).setName('Calendar behavior').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Current time indicator')
|
||
.setDesc('Show a line indicating the current time in week and day views')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show current time indicator');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.nowIndicator)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.nowIndicator = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Selection mirror')
|
||
.setDesc('Show a visual preview while dragging to select time ranges')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Enable selection mirror');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.selectMirror)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.selectMirror = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Highlight today')
|
||
.setDesc('Highlight the current date with a background color in calendar views')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Highlight today');
|
||
return toggle
|
||
.setValue(this.plugin.settings.calendarViewSettings.showTodayHighlight)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.calendarViewSettings.showTodayHighlight = value;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
// ICS Calendar Subscriptions section
|
||
new Setting(container).setName('Calendar subscriptions').setHeading();
|
||
|
||
// Description section
|
||
container.createEl('p', {
|
||
text: 'Subscribe to external calendar feeds (ICS/iCal format) to display events alongside your tasks.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Subscription list
|
||
const subscriptionList = container.createDiv('settings-list settings-view__list');
|
||
this.renderSubscriptionList(subscriptionList);
|
||
|
||
// Add subscription form
|
||
new Setting(container)
|
||
.setName('Add calendar subscription')
|
||
.setDesc('Subscribe to an external calendar feed or add a local ICS file');
|
||
|
||
const addForm = container.createDiv('ics-add-subscription-form');
|
||
|
||
// Type selection
|
||
const typeRow = addForm.createDiv('ics-form-row');
|
||
typeRow.createEl('label', { text: 'Source type:', cls: 'ics-form-label' });
|
||
const typeSelect = typeRow.createEl('select', { cls: 'ics-form-select' });
|
||
typeSelect.createEl('option', { value: 'remote', text: 'Remote URL' });
|
||
typeSelect.createEl('option', { value: 'local', text: 'Local file' });
|
||
|
||
// Name input
|
||
const nameRow = addForm.createDiv('ics-form-row');
|
||
nameRow.createEl('label', { text: 'Name:', cls: 'ics-form-label' });
|
||
const nameInput = nameRow.createEl('input', {
|
||
type: 'text',
|
||
placeholder: 'My Calendar',
|
||
cls: 'ics-form-input'
|
||
});
|
||
|
||
// URL input (for remote)
|
||
const urlRow = addForm.createDiv('ics-form-row');
|
||
urlRow.createEl('label', { text: 'ICS URL:', cls: 'ics-form-label' });
|
||
const urlInput = urlRow.createEl('input', {
|
||
type: 'url',
|
||
placeholder: 'https://example.com/calendar.ics',
|
||
cls: 'ics-form-input'
|
||
});
|
||
|
||
// Local file selection (for local)
|
||
const fileRow = addForm.createDiv('ics-form-row ics-form-row-hidden');
|
||
fileRow.createEl('label', { text: 'ICS file:', cls: 'ics-form-label' });
|
||
const fileSelect = fileRow.createEl('select', { cls: 'ics-form-select' });
|
||
|
||
// Function to update available ICS files
|
||
const updateLocalFiles = () => {
|
||
fileSelect.empty();
|
||
fileSelect.createEl('option', { value: '', text: 'Select an ICS file...' });
|
||
|
||
const icsFiles = this.plugin.icsSubscriptionService?.getLocalICSFiles() || [];
|
||
icsFiles.forEach(file => {
|
||
fileSelect.createEl('option', { value: file.path, text: file.path });
|
||
});
|
||
|
||
if (icsFiles.length === 0) {
|
||
fileSelect.createEl('option', { value: '', text: 'No .ics files found in vault', attr: { disabled: 'true' } });
|
||
}
|
||
};
|
||
|
||
// Initial update
|
||
updateLocalFiles();
|
||
|
||
// Type change handler
|
||
typeSelect.addEventListener('change', () => {
|
||
const isRemote = typeSelect.value === 'remote';
|
||
urlRow.style.display = isRemote ? 'flex' : 'none';
|
||
fileRow.style.display = isRemote ? 'none' : 'flex';
|
||
|
||
if (!isRemote) {
|
||
updateLocalFiles();
|
||
}
|
||
});
|
||
|
||
// Color and settings row
|
||
const settingsRow = addForm.createDiv('ics-form-row ics-form-row-multi');
|
||
|
||
// Color input
|
||
const colorGroup = settingsRow.createDiv('ics-form-group');
|
||
colorGroup.createEl('label', { text: 'Color:', cls: 'ics-form-label' });
|
||
const colorInput = colorGroup.createEl('input', {
|
||
type: 'color',
|
||
value: '#3788d8',
|
||
cls: 'ics-form-color'
|
||
});
|
||
|
||
// Refresh interval input
|
||
const intervalGroup = settingsRow.createDiv('ics-form-group');
|
||
intervalGroup.createEl('label', { text: 'Refresh (min):', cls: 'ics-form-label' });
|
||
const intervalInput = intervalGroup.createEl('input', {
|
||
type: 'number',
|
||
value: '60',
|
||
cls: 'ics-form-number'
|
||
});
|
||
intervalInput.setAttribute('min', '15');
|
||
intervalInput.setAttribute('max', '1440');
|
||
intervalInput.setAttribute('step', '15');
|
||
|
||
// Enabled checkbox
|
||
const enabledGroup = settingsRow.createDiv('ics-form-group');
|
||
const enabledLabel = enabledGroup.createEl('label', { cls: 'ics-form-checkbox-label' });
|
||
const enabledCheckbox = enabledLabel.createEl('input', {
|
||
type: 'checkbox',
|
||
cls: 'ics-form-checkbox'
|
||
});
|
||
enabledCheckbox.checked = true;
|
||
enabledLabel.createSpan({ text: ' Enabled' });
|
||
|
||
// Add button
|
||
const buttonRow = addForm.createDiv('ics-form-row');
|
||
const addButton = buttonRow.createEl('button', {
|
||
text: 'Add Subscription',
|
||
cls: 'ics-form-button mod-cta'
|
||
});
|
||
|
||
addButton.addEventListener('click', async () => {
|
||
const name = nameInput.value.trim();
|
||
const type = typeSelect.value as 'remote' | 'local';
|
||
const url = urlInput.value.trim();
|
||
const filePath = fileSelect.value.trim();
|
||
const color = colorInput.value;
|
||
const refreshInterval = parseInt(intervalInput.value);
|
||
const enabled = enabledCheckbox.checked;
|
||
|
||
if (!name) {
|
||
new Notice('Name is required');
|
||
return;
|
||
}
|
||
|
||
if (type === 'remote' && !url) {
|
||
new Notice('URL is required for remote subscriptions');
|
||
return;
|
||
}
|
||
|
||
if (type === 'local' && !filePath) {
|
||
new Notice('Please select a local ICS file');
|
||
return;
|
||
}
|
||
|
||
if (refreshInterval < 15 || refreshInterval > 1440) {
|
||
new Notice('Refresh interval must be between 15 and 1440 minutes');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
addButton.textContent = 'Adding...';
|
||
addButton.disabled = true;
|
||
|
||
const subscriptionData = {
|
||
name,
|
||
type,
|
||
color,
|
||
refreshInterval,
|
||
enabled,
|
||
...(type === 'remote' ? { url } : { filePath })
|
||
};
|
||
|
||
await this.plugin.icsSubscriptionService!.addSubscription(subscriptionData);
|
||
|
||
new Notice(`Added ${type} subscription "${name}"`);
|
||
|
||
// Clear the form
|
||
nameInput.value = '';
|
||
urlInput.value = '';
|
||
fileSelect.value = '';
|
||
colorInput.value = '#3788d8';
|
||
intervalInput.value = '60';
|
||
enabledCheckbox.checked = true;
|
||
typeSelect.value = 'remote';
|
||
urlRow.style.display = 'flex';
|
||
fileRow.style.display = 'none';
|
||
|
||
// Refresh the subscription list
|
||
this.renderActiveTab();
|
||
} catch (error) {
|
||
console.error('Error adding subscription:', error);
|
||
new Notice('Failed to add subscription');
|
||
} finally {
|
||
addButton.textContent = 'Add Subscription';
|
||
addButton.disabled = false;
|
||
}
|
||
});
|
||
|
||
// Refresh all button
|
||
new Setting(container)
|
||
.setName('Refresh all subscriptions')
|
||
.setDesc('Manually refresh all enabled calendar subscriptions')
|
||
.addButton(button => button
|
||
.setButtonText('Refresh all')
|
||
.onClick(async () => {
|
||
if (this.plugin.icsSubscriptionService) {
|
||
button.setButtonText('Refreshing...');
|
||
button.setDisabled(true);
|
||
try {
|
||
await this.plugin.icsSubscriptionService.refreshAllSubscriptions();
|
||
new Notice('All calendar subscriptions refreshed successfully');
|
||
} catch (error) {
|
||
console.error('Error refreshing subscriptions:', error);
|
||
new Notice('Failed to refresh some calendar subscriptions');
|
||
} finally {
|
||
button.setButtonText('Refresh all');
|
||
button.setDisabled(false);
|
||
}
|
||
}
|
||
}));
|
||
|
||
// Help section
|
||
const helpContainer = container.createDiv('settings-help-section');
|
||
helpContainer.createEl('h4', { text: 'Calendar sources:' });
|
||
|
||
// Remote URLs section
|
||
helpContainer.createEl('h5', { text: 'Remote calendar URLs:' });
|
||
const urlHelpList = helpContainer.createEl('ul');
|
||
urlHelpList.createEl('li', { text: 'Google Calendar: Settings → Calendar settings → Integrate calendar → Secret address in iCal format' });
|
||
urlHelpList.createEl('li', { text: 'Outlook/Office 365: Calendar settings → Share calendar → Publish a calendar → ICS format' });
|
||
urlHelpList.createEl('li', { text: 'Other services: Look for "Calendar subscription", "ICS feed", "iCal URL", or "Webcal" options' });
|
||
|
||
// Local files section
|
||
helpContainer.createEl('h5', { text: 'Local ICS files:' });
|
||
const fileHelpList = helpContainer.createEl('ul');
|
||
fileHelpList.createEl('li', { text: 'Place .ics files anywhere in your vault' });
|
||
fileHelpList.createEl('li', { text: 'Export from Apple Calendar: File → Export → Export as .ics file' });
|
||
fileHelpList.createEl('li', { text: 'Export from Google Calendar: Settings → Export → Download your data' });
|
||
fileHelpList.createEl('li', { text: 'Files are automatically watched for changes and refreshed' });
|
||
|
||
helpContainer.createEl('p', {
|
||
text: 'Important: For Google Calendar remote URLs, you must use the "Secret address" (private URL) from your calendar settings. The calendar must be set to "Make available to public" for the secret URL to work.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
helpContainer.createEl('p', {
|
||
text: 'Note: Only read-only access is supported. You cannot edit calendar events from within TaskNotes.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// ICS Integration Settings
|
||
new Setting(container).setName('Content creation from events').setHeading();
|
||
|
||
new Setting(container)
|
||
.setName('Default note template')
|
||
.setDesc('Template file for notes created from ICS events (leave empty for default format)')
|
||
.addText(text => text
|
||
.setValue(this.plugin.settings.icsIntegration?.defaultNoteTemplate || '')
|
||
.setPlaceholder('templates/ics-note-template.md')
|
||
.onChange(async (value) => {
|
||
if (!this.plugin.settings.icsIntegration) {
|
||
this.plugin.settings.icsIntegration = {
|
||
defaultNoteTemplate: '',
|
||
defaultNoteFolder: ''
|
||
};
|
||
}
|
||
this.plugin.settings.icsIntegration.defaultNoteTemplate = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Default note folder')
|
||
.setDesc('Folder for notes created from ICS events (leave empty for vault root)')
|
||
.addText(text => text
|
||
.setValue(this.plugin.settings.icsIntegration?.defaultNoteFolder || '')
|
||
.setPlaceholder('Notes/Calendar Events')
|
||
.onChange(async (value) => {
|
||
if (!this.plugin.settings.icsIntegration) {
|
||
this.plugin.settings.icsIntegration = {
|
||
defaultNoteTemplate: '',
|
||
defaultNoteFolder: ''
|
||
};
|
||
}
|
||
this.plugin.settings.icsIntegration.defaultNoteFolder = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
}
|
||
|
||
private renderNotificationsTab(): void {
|
||
const container = this.tabContents['notifications'];
|
||
|
||
new Setting(container).setName('Notifications').setHeading();
|
||
|
||
container.createEl('p', {
|
||
text: 'Configure task reminder notifications.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Enable reminders')
|
||
.setDesc('Enable the task reminder system. When disabled, no reminder notifications will be shown.')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableNotifications)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableNotifications = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Notification type')
|
||
.setDesc('Choose how reminder notifications are displayed.')
|
||
.addDropdown(dropdown => dropdown
|
||
.addOption('system', 'System notifications')
|
||
.addOption('in-app', 'In-app notices')
|
||
.setValue(this.plugin.settings.notificationType)
|
||
.onChange(async (value: 'system' | 'in-app') => {
|
||
this.plugin.settings.notificationType = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
// Additional info about system notifications
|
||
const systemNotesEl = container.createDiv({ cls: 'setting-item-description' });
|
||
systemNotesEl.innerHTML = `
|
||
<strong>System notifications:</strong> Use your operating system's native notification system.
|
||
Requires permission and works even when Obsidian is minimized.<br>
|
||
<strong>In-app notices:</strong> Show notifications as temporary popups within Obsidian only.
|
||
`;
|
||
|
||
}
|
||
|
||
private renderAPITab(): void {
|
||
const container = this.tabContents['api'];
|
||
|
||
// Show message on mobile
|
||
if (Platform.isMobile) {
|
||
const mobileMessage = container.createDiv({ cls: 'setting-item-description' });
|
||
mobileMessage.innerHTML = `
|
||
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
||
<h3>HTTP API not available on mobile</h3>
|
||
<p>The HTTP API feature requires Node.js capabilities that are only available on desktop platforms.</p>
|
||
<p>This tab will be available when using TaskNotes on desktop.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.createEl('h2', { text: 'HTTP API Settings' });
|
||
|
||
// API description
|
||
const descEl = container.createDiv({ cls: 'setting-item-description' });
|
||
descEl.innerHTML = `
|
||
<p>Enable HTTP API server to allow external tools and scripts to interact with your TaskNotes data.</p>
|
||
<p><strong>Note:</strong> This feature is only available on desktop. Restart Obsidian after changing API settings.</p>
|
||
`;
|
||
|
||
new Setting(container)
|
||
.setName('Enable HTTP API')
|
||
.setDesc('Enable HTTP API server for external tool integration.')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.enableAPI)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.enableAPI = value;
|
||
await this.plugin.saveSettings();
|
||
if (value) {
|
||
new Notice('API enabled. Restart Obsidian to start the server.');
|
||
} else {
|
||
new Notice('API disabled. Restart Obsidian to stop the server.');
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('API Port')
|
||
.setDesc('Port for the HTTP API server (default: 8080)')
|
||
.addText(text => text
|
||
.setPlaceholder('8080')
|
||
.setValue(this.plugin.settings.apiPort.toString())
|
||
.onChange(async (value) => {
|
||
const port = parseInt(value) || 8080;
|
||
if (port < 1024 || port > 65535) {
|
||
new Notice('Port must be between 1024 and 65535');
|
||
return;
|
||
}
|
||
this.plugin.settings.apiPort = port;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('API Authentication Token')
|
||
.setDesc('Optional token for API authentication. Leave empty to disable authentication.')
|
||
.addText(text => text
|
||
.setPlaceholder('Optional authentication token')
|
||
.setValue(this.plugin.settings.apiAuthToken)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.apiAuthToken = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
// Webhook settings section
|
||
container.createEl('h3', { text: 'Webhook Settings' });
|
||
|
||
const webhookDescEl = container.createDiv({ cls: 'setting-item-description' });
|
||
webhookDescEl.innerHTML = `
|
||
<p>Webhooks send real-time notifications to external services when TaskNotes events occur.</p>
|
||
<p>Configure webhooks to integrate with automation tools, sync services, or custom applications.</p>
|
||
`;
|
||
|
||
// Webhook management
|
||
this.renderWebhookList(container);
|
||
|
||
// Add webhook button
|
||
new Setting(container)
|
||
.setName('Add Webhook')
|
||
.setDesc('Register a new webhook endpoint')
|
||
.addButton(button => button
|
||
.setButtonText('Add Webhook')
|
||
.setTooltip('Add a new webhook endpoint')
|
||
.onClick(() => {
|
||
this.showWebhookModal();
|
||
}));
|
||
|
||
// API documentation section
|
||
container.createEl('h3', { text: 'API Documentation' });
|
||
|
||
const apiInfoEl = container.createDiv({ cls: 'setting-item-description' });
|
||
apiInfoEl.innerHTML = `
|
||
<h4>Available Endpoints:</h4>
|
||
<ul style="margin-left: 1rem;">
|
||
<li><code>GET /api/health</code> - Health check</li>
|
||
<li><code>GET /api/tasks</code> - List tasks with optional filters</li>
|
||
<li><code>POST /api/tasks</code> - Create new task</li>
|
||
<li><code>GET /api/tasks/{id}</code> - Get specific task</li>
|
||
<li><code>PUT /api/tasks/{id}</code> - Update task</li>
|
||
<li><code>DELETE /api/tasks/{id}</code> - Delete task</li>
|
||
<li><code>POST /api/tasks/{id}/time/start</code> - Start time tracking</li>
|
||
<li><code>POST /api/tasks/{id}/time/stop</code> - Stop time tracking</li>
|
||
<li><code>POST /api/tasks/{id}/toggle-status</code> - Toggle completion</li>
|
||
<li><code>POST /api/tasks/{id}/archive</code> - Toggle archive</li>
|
||
<li><code>POST /api/tasks/query</code> - Advanced filtering</li>
|
||
<li><code>GET /api/filter-options</code> - Available filters</li>
|
||
<li><code>GET /api/stats</code> - Task statistics</li>
|
||
</ul>
|
||
|
||
<h4>Usage Examples:</h4>
|
||
<p><strong>Basic request:</strong></p>
|
||
<pre><code>curl http://localhost:${this.plugin.settings.apiPort}/api/tasks</code></pre>
|
||
|
||
<p><strong>With authentication:</strong></p>
|
||
<pre><code>curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:${this.plugin.settings.apiPort}/api/tasks</code></pre>
|
||
|
||
<p><strong>Create task:</strong></p>
|
||
<pre><code>curl -X POST http://localhost:${this.plugin.settings.apiPort}/api/tasks \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{"title": "New task", "priority": "High"}'</code></pre>
|
||
|
||
<p><strong>Filter tasks:</strong></p>
|
||
<pre><code>curl "http://localhost:${this.plugin.settings.apiPort}/api/tasks?status=open&priority=High"</code></pre>
|
||
`;
|
||
}
|
||
|
||
private renderMiscTab(): void {
|
||
const container = this.tabContents['misc'];
|
||
|
||
// Misc settings
|
||
new Setting(container).setName('Miscellaneous settings').setHeading();
|
||
|
||
container.createEl('p', {
|
||
text: 'Configure various plugin features and display options.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Status bar toggle
|
||
new Setting(container)
|
||
.setName('Show tracked tasks in status bar')
|
||
.setDesc('Display currently tracked tasks (with active time tracking) in the status bar at the bottom of the app')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show tracked tasks in status bar');
|
||
return toggle
|
||
.setValue(this.plugin.settings.showTrackedTasksInStatusBar)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.showTrackedTasksInStatusBar = value;
|
||
await this.plugin.saveSettings();
|
||
// Update status bar visibility immediately
|
||
if (this.plugin.statusBarService) {
|
||
this.plugin.statusBarService.updateVisibility();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Project subtasks widget toggle
|
||
new Setting(container)
|
||
.setName('Show project subtasks widget')
|
||
.setDesc('Display a collapsible widget showing all tasks that reference the current note as a project')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show project subtasks widget');
|
||
return toggle
|
||
.setValue(this.plugin.settings.showProjectSubtasks)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.showProjectSubtasks = value;
|
||
await this.plugin.saveSettings();
|
||
// Refresh all open editors to apply the change
|
||
this.plugin.notifyDataChanged();
|
||
});
|
||
});
|
||
|
||
// Project subtasks widget position
|
||
new Setting(container)
|
||
.setName('Project subtasks widget position')
|
||
.setDesc('Choose where the subtasks widget appears in project notes')
|
||
.addDropdown(dropdown => {
|
||
dropdown
|
||
.addOption('top', 'Top of note')
|
||
.addOption('bottom', 'Bottom of note')
|
||
.setValue(this.plugin.settings.projectSubtasksPosition || 'bottom')
|
||
.onChange(async (value: 'top' | 'bottom') => {
|
||
this.plugin.settings.projectSubtasksPosition = value;
|
||
await this.plugin.saveSettings();
|
||
// Refresh all open editors to apply the change
|
||
this.plugin.notifyDataChanged();
|
||
});
|
||
});
|
||
|
||
// Expandable subtasks in task cards
|
||
new Setting(container)
|
||
.setName('Show expandable subtasks in task cards')
|
||
.setDesc('Add a chevron icon to project task cards that allows expanding to view subtasks inline')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Show expandable subtasks in task cards');
|
||
return toggle
|
||
.setValue(this.plugin.settings.showExpandableSubtasks)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.showExpandableSubtasks = value;
|
||
await this.plugin.saveSettings();
|
||
// Refresh task views to apply the change
|
||
this.plugin.notifyDataChanged();
|
||
});
|
||
});
|
||
// Hide completed tasks from overdue
|
||
new Setting(container)
|
||
.setName('Hide completed tasks from overdue')
|
||
.setDesc('When enabled, completed tasks will not appear as overdue in the agenda view, even if their due/scheduled date has passed')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Hide completed tasks from overdue status');
|
||
return toggle
|
||
.setValue(this.plugin.settings.hideCompletedFromOverdue)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.hideCompletedFromOverdue = value;
|
||
await this.plugin.saveSettings();
|
||
// Refresh views to apply the change
|
||
this.plugin.notifyDataChanged();
|
||
});
|
||
});
|
||
|
||
// Notes indexing toggle
|
||
new Setting(container)
|
||
.setName('Disable note indexing')
|
||
.setDesc('Disable indexing and caching of non-task notes to improve performance in large vaults. Note: This will disable the Notes view and notes display in the Agenda view. Requires plugin restart to take effect.')
|
||
.addToggle(toggle => {
|
||
toggle.toggleEl.setAttribute('aria-label', 'Disable note indexing for better performance');
|
||
return toggle
|
||
.setValue(this.plugin.settings.disableNoteIndexing)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.disableNoteIndexing = value;
|
||
await this.plugin.saveSettings();
|
||
|
||
// Show notice about restart requirement
|
||
new Notice('Note indexing setting changed. Please restart Obsidian or reload the plugin for changes to take effect.');
|
||
});
|
||
});
|
||
}
|
||
|
||
private renderFieldMappingTab(): void {
|
||
const container = this.tabContents['field-mapping'];
|
||
|
||
// Warning message
|
||
const warning = container.createDiv('settings-warning settings-view__warning');
|
||
const warningIcon = warning.createEl('strong', { cls: 'settings-view__warning-icon' });
|
||
setIcon(warningIcon, 'alert-triangle');
|
||
warningIcon.createSpan({ text: ' Warning:' });
|
||
warning.createSpan({ text: ' TaskNotes will read AND write using these property names. Changing these after creating tasks may cause inconsistencies.' });
|
||
|
||
new Setting(container)
|
||
.setName('Field mapping')
|
||
.setHeading();
|
||
container.createEl('p', {
|
||
text: 'Configure which frontmatter properties TaskNotes should use for each field.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Create mapping table
|
||
const table = container.createEl('table', { cls: 'settings-table settings-view__table' });
|
||
|
||
const header = table.createEl('tr');
|
||
header.createEl('th', { cls: 'settings-view__table-header', text: 'TaskNotes field' });
|
||
header.createEl('th', { cls: 'settings-view__table-header', text: 'Your property name' });
|
||
|
||
const fieldMappings: Array<[keyof FieldMapping, string]> = [
|
||
['title', 'Title'],
|
||
['status', 'Status'],
|
||
['priority', 'Priority'],
|
||
['due', 'Due date'],
|
||
['scheduled', 'Scheduled date'],
|
||
['contexts', 'Contexts'],
|
||
['projects', 'Projects'],
|
||
['timeEstimate', 'Time estimate'],
|
||
['completedDate', 'Completed date'],
|
||
['dateCreated', 'Created date'],
|
||
['dateModified', 'Modified date'],
|
||
['recurrence', 'Recurrence'],
|
||
['archiveTag', 'Archive tag'],
|
||
['icsEventId', 'ICS Event ID'],
|
||
['icsEventTag', 'ICS Event Tag']
|
||
];
|
||
|
||
fieldMappings.forEach(([field, label]) => {
|
||
const row = table.createEl('tr', { cls: 'settings-view__table-row' });
|
||
const labelCell = row.createEl('td', { cls: 'settings-view__table-cell' });
|
||
labelCell.textContent = label;
|
||
|
||
const inputCell = row.createEl('td', { cls: 'settings-view__table-cell' });
|
||
|
||
const input = inputCell.createEl('input', {
|
||
type: 'text',
|
||
value: this.plugin.settings.fieldMapping[field],
|
||
cls: 'settings-view__table-input',
|
||
attr: {
|
||
'aria-label': `Property name for ${label}`,
|
||
'id': `field-mapping-${field}`
|
||
}
|
||
});
|
||
|
||
input.addEventListener('change', async () => {
|
||
try {
|
||
this.plugin.settings.fieldMapping[field] = input.value;
|
||
await this.plugin.saveSettings();
|
||
} catch (error) {
|
||
console.error(`Error updating field mapping for ${field}:`, error);
|
||
new Notice(`Failed to update field mapping for ${label}. Please try again.`);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Reset button
|
||
new Setting(container)
|
||
.setName('Reset to defaults')
|
||
.setDesc('Reset all field mappings to default values')
|
||
.addButton(button => button
|
||
.setButtonText('Reset')
|
||
.setWarning()
|
||
.onClick(async () => {
|
||
this.plugin.settings.fieldMapping = { ...DEFAULT_FIELD_MAPPING };
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab();
|
||
}));
|
||
}
|
||
|
||
private renderStatusesTab(): void {
|
||
const container = this.tabContents['statuses'];
|
||
|
||
new Setting(container)
|
||
.setName('Task statuses')
|
||
.setHeading();
|
||
|
||
// Description section
|
||
container.createEl('p', {
|
||
text: 'Customize the status options available for your tasks. These statuses control the task lifecycle and determine when tasks are considered complete.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Help section
|
||
const helpContainer = container.createDiv('settings-help-section');
|
||
helpContainer.createEl('h4', { text: 'How statuses work:' });
|
||
const helpList = helpContainer.createEl('ul');
|
||
helpList.createEl('li', { text: 'Value: The internal identifier stored in your task files (e.g., "in-progress")' });
|
||
helpList.createEl('li', { text: 'Label: The display name shown in the interface (e.g., "In Progress")' });
|
||
helpList.createEl('li', { text: 'Color: Visual indicator color for the status dot and badges' });
|
||
helpList.createEl('li', { text: 'Completed: When checked, tasks with this status are considered finished and may be filtered differently' });
|
||
|
||
helpContainer.createEl('p', {
|
||
text: 'The order below determines the sequence when cycling through statuses by clicking on task status badges.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Column headers
|
||
const headersRow = container.createDiv('settings-headers-row settings-view__list-headers');
|
||
headersRow.createDiv('settings-header-spacer settings-view__header-spacer'); // For drag handle space
|
||
headersRow.createDiv('settings-header-spacer settings-view__header-spacer'); // For color indicator space
|
||
headersRow.createEl('span', { text: 'Value', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createEl('span', { text: 'Display Label', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createEl('span', { text: 'Color', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createEl('span', { text: 'Mark as Completed', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createDiv('settings-header-spacer settings-view__header-spacer'); // For delete button space
|
||
|
||
// Status list
|
||
const statusList = container.createDiv('settings-list settings-view__list');
|
||
|
||
this.renderStatusList(statusList);
|
||
|
||
// Add status button
|
||
new Setting(container)
|
||
.setName('Add new status')
|
||
.setDesc('Create a new status option for your tasks')
|
||
.addButton(button => button
|
||
.setButtonText('Add status')
|
||
.onClick(async () => {
|
||
const newStatus = StatusManager.createDefaultStatus(this.plugin.settings.customStatuses);
|
||
this.plugin.settings.customStatuses.push(newStatus);
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab();
|
||
}));
|
||
|
||
// Validation note
|
||
container.createEl('p', {
|
||
text: 'Note: You must have at least 2 statuses, and at least one status must be marked as "Completed".',
|
||
cls: 'settings-validation-note'
|
||
});
|
||
}
|
||
|
||
private renderStatusList(container: HTMLElement): void {
|
||
container.empty();
|
||
|
||
const sortedStatuses = [...this.plugin.settings.customStatuses].sort((a, b) => a.order - b.order);
|
||
|
||
sortedStatuses.forEach((status, index) => {
|
||
const statusRow = container.createDiv('settings-item-row settings-view__item-row');
|
||
statusRow.setAttribute('draggable', 'true');
|
||
statusRow.setAttribute('data-status-id', status.id);
|
||
|
||
// Drag handle
|
||
const dragHandle = statusRow.createDiv('settings-drag-handle');
|
||
dragHandle.textContent = '☰';
|
||
setTooltip(dragHandle, 'Drag to reorder', { placement: 'top' });
|
||
|
||
// Color indicator
|
||
const colorIndicator = statusRow.createDiv('settings-color-indicator settings-view__color-indicator');
|
||
colorIndicator.style.setProperty('--indicator-color', status.color);
|
||
|
||
// Status value input
|
||
const valueInput = statusRow.createEl('input', {
|
||
type: 'text',
|
||
value: status.value,
|
||
cls: 'settings-input value-input settings-view__input settings-view__input--value',
|
||
attr: {
|
||
'aria-label': `Status value for ${status.label}`,
|
||
'id': `status-value-${status.id}`
|
||
}
|
||
});
|
||
|
||
// Status label input
|
||
const labelInput = statusRow.createEl('input', {
|
||
type: 'text',
|
||
value: status.label,
|
||
cls: 'settings-input label-input settings-view__input settings-view__input--label',
|
||
attr: {
|
||
'aria-label': `Display label for ${status.label} status`,
|
||
'id': `status-label-${status.id}`
|
||
}
|
||
});
|
||
|
||
// Color input
|
||
const colorInput = statusRow.createEl('input', {
|
||
type: 'color',
|
||
value: status.color,
|
||
cls: 'settings-input color-input settings-view__input settings-view__input--color',
|
||
attr: {
|
||
'aria-label': `Color for ${status.label} status`,
|
||
'id': `status-color-${status.id}`
|
||
}
|
||
});
|
||
|
||
// Completed checkbox
|
||
const completedLabel = statusRow.createEl('label', {
|
||
cls: 'settings-checkbox-label settings-view__checkbox-label',
|
||
attr: { 'for': `status-completed-${status.id}` }
|
||
});
|
||
|
||
const completedCheckbox = completedLabel.createEl('input', {
|
||
type: 'checkbox',
|
||
cls: 'settings-view__checkbox',
|
||
attr: {
|
||
'id': `status-completed-${status.id}`,
|
||
'aria-label': `Mark ${status.label} as completed status`
|
||
}
|
||
});
|
||
completedCheckbox.checked = status.isCompleted;
|
||
|
||
completedLabel.createSpan({ text: 'Completed' });
|
||
|
||
// Delete button
|
||
const deleteButton = statusRow.createEl('button', {
|
||
text: 'Delete',
|
||
cls: 'settings-delete-button settings-view__delete-button'
|
||
});
|
||
|
||
// Event listeners
|
||
const updateStatus = async () => {
|
||
try {
|
||
status.value = valueInput.value;
|
||
status.label = labelInput.value;
|
||
status.color = colorInput.value;
|
||
status.isCompleted = completedCheckbox.checked;
|
||
await this.plugin.saveSettings();
|
||
colorIndicator.style.setProperty('--indicator-color', status.color);
|
||
} catch (error) {
|
||
console.error('Error updating status configuration:', error);
|
||
new Notice('Failed to update status configuration. Please try again.');
|
||
}
|
||
};
|
||
|
||
valueInput.addEventListener('change', updateStatus);
|
||
labelInput.addEventListener('change', updateStatus);
|
||
colorInput.addEventListener('change', updateStatus);
|
||
completedCheckbox.addEventListener('change', updateStatus);
|
||
|
||
deleteButton.addEventListener('click', async () => {
|
||
if (this.plugin.settings.customStatuses.length <= 2) {
|
||
new Notice('You must have at least 2 statuses');
|
||
return;
|
||
}
|
||
|
||
// Show confirmation dialog using Obsidian's Modal API
|
||
const confirmed = await showConfirmationModal(this.app, {
|
||
title: 'Delete Status',
|
||
message: `Are you sure you want to delete the status "${status.label}"?\n\nThis action cannot be undone and may affect existing tasks.`,
|
||
confirmText: 'Delete',
|
||
cancelText: 'Cancel',
|
||
isDestructive: true
|
||
});
|
||
|
||
if (confirmed) {
|
||
const statusIndex = this.plugin.settings.customStatuses.findIndex(s => s.id === status.id);
|
||
if (statusIndex !== -1) {
|
||
this.plugin.settings.customStatuses.splice(statusIndex, 1);
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Drag and drop event handlers
|
||
statusRow.addEventListener('dragstart', (e) => {
|
||
e.dataTransfer!.setData('text/plain', status.id);
|
||
statusRow.classList.add('dragging');
|
||
});
|
||
|
||
statusRow.addEventListener('dragend', () => {
|
||
statusRow.classList.remove('dragging');
|
||
});
|
||
|
||
statusRow.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
const draggingRow = container.querySelector('.dragging') as HTMLElement;
|
||
if (draggingRow && draggingRow !== statusRow) {
|
||
const rect = statusRow.getBoundingClientRect();
|
||
const midpoint = rect.top + rect.height / 2;
|
||
if (e.clientY < midpoint) {
|
||
statusRow.classList.add('drag-over-top');
|
||
statusRow.classList.remove('drag-over-bottom');
|
||
} else {
|
||
statusRow.classList.add('drag-over-bottom');
|
||
statusRow.classList.remove('drag-over-top');
|
||
}
|
||
}
|
||
});
|
||
|
||
statusRow.addEventListener('dragleave', () => {
|
||
statusRow.classList.remove('drag-over-top', 'drag-over-bottom');
|
||
});
|
||
|
||
statusRow.addEventListener('drop', async (e) => {
|
||
e.preventDefault();
|
||
statusRow.classList.remove('drag-over-top', 'drag-over-bottom');
|
||
|
||
const draggedStatusId = e.dataTransfer!.getData('text/plain');
|
||
const targetStatusId = status.id;
|
||
|
||
if (draggedStatusId !== targetStatusId) {
|
||
await this.reorderStatus(draggedStatusId, targetStatusId, e.clientY < statusRow.getBoundingClientRect().top + statusRow.getBoundingClientRect().height / 2);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
private async reorderStatus(draggedStatusId: string, targetStatusId: string, insertBefore: boolean): Promise<void> {
|
||
const statuses = [...this.plugin.settings.customStatuses];
|
||
|
||
// Find the dragged and target statuses
|
||
const draggedIndex = statuses.findIndex(s => s.id === draggedStatusId);
|
||
const targetIndex = statuses.findIndex(s => s.id === targetStatusId);
|
||
|
||
if (draggedIndex === -1 || targetIndex === -1) {
|
||
return;
|
||
}
|
||
|
||
// Remove the dragged status from its current position
|
||
const [draggedStatus] = statuses.splice(draggedIndex, 1);
|
||
|
||
// Determine the new position
|
||
let newIndex = targetIndex;
|
||
if (draggedIndex < targetIndex) {
|
||
// If we removed an item before the target, adjust the target index
|
||
newIndex--;
|
||
}
|
||
|
||
if (!insertBefore) {
|
||
// Insert after the target
|
||
newIndex++;
|
||
}
|
||
|
||
// Insert the dragged status at the new position
|
||
statuses.splice(newIndex, 0, draggedStatus);
|
||
|
||
// Update the order values
|
||
statuses.forEach((status, index) => {
|
||
status.order = index;
|
||
});
|
||
|
||
// Save the updated statuses
|
||
this.plugin.settings.customStatuses = statuses;
|
||
await this.plugin.saveSettings();
|
||
|
||
// Re-render the list to reflect the new order
|
||
this.renderActiveTab();
|
||
}
|
||
|
||
private renderPrioritiesTab(): void {
|
||
const container = this.tabContents['priorities'];
|
||
|
||
new Setting(container)
|
||
.setName('Task priorities')
|
||
.setHeading();
|
||
|
||
// Description section
|
||
container.createEl('p', {
|
||
text: 'Customize the priority levels available for your tasks. Priority weights determine sorting order and visual hierarchy in your task views.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Help section
|
||
const helpContainer = container.createDiv('settings-help-section');
|
||
helpContainer.createEl('h4', { text: 'How priorities work:' });
|
||
const helpList = helpContainer.createEl('ul');
|
||
helpList.createEl('li', { text: 'Value: The internal identifier stored in your task files (e.g., "high")' });
|
||
helpList.createEl('li', { text: 'Display Label: The display name shown in the interface (e.g., "High Priority")' });
|
||
helpList.createEl('li', { text: 'Color: Visual indicator color for the priority dot and badges' });
|
||
helpList.createEl('li', { text: 'Weight: Numeric value for sorting (higher weights appear first in lists)' });
|
||
|
||
helpContainer.createEl('p', {
|
||
text: 'Tasks are automatically sorted by priority weight in descending order (highest weight first). Weights can be any positive number.',
|
||
cls: 'settings-help-note'
|
||
});
|
||
|
||
// Column headers
|
||
const headersRow = container.createDiv('settings-headers-row settings-view__list-headers');
|
||
headersRow.createDiv('settings-header-spacer settings-view__header-spacer'); // For color indicator space
|
||
headersRow.createEl('span', { text: 'Value', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createEl('span', { text: 'Display Label', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createEl('span', { text: 'Color', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createEl('span', { text: 'Weight', cls: 'settings-column-header settings-view__column-header' });
|
||
headersRow.createDiv('settings-header-spacer settings-view__header-spacer'); // For delete button space
|
||
|
||
// Priority list
|
||
const priorityList = container.createDiv('settings-list settings-view__list');
|
||
|
||
this.renderPriorityList(priorityList);
|
||
|
||
// Add priority button
|
||
new Setting(container)
|
||
.setName('Add new priority')
|
||
.setDesc('Create a new priority level for your tasks')
|
||
.addButton(button => button
|
||
.setButtonText('Add priority')
|
||
.onClick(async () => {
|
||
const newPriority = PriorityManager.createDefaultPriority(this.plugin.settings.customPriorities);
|
||
this.plugin.settings.customPriorities.push(newPriority);
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab();
|
||
}));
|
||
|
||
// Validation note
|
||
container.createEl('p', {
|
||
text: 'Note: You must have at least 1 priority. Higher weights take precedence in sorting and visual hierarchy.',
|
||
cls: 'settings-validation-note'
|
||
});
|
||
}
|
||
|
||
private renderPriorityList(container: HTMLElement): void {
|
||
container.empty();
|
||
|
||
const sortedPriorities = [...this.plugin.settings.customPriorities].sort((a, b) => b.weight - a.weight);
|
||
|
||
sortedPriorities.forEach((priority, index) => {
|
||
const priorityRow = container.createDiv('settings-item-row settings-view__item-row');
|
||
|
||
// Color indicator
|
||
const colorIndicator = priorityRow.createDiv('settings-color-indicator settings-view__color-indicator');
|
||
colorIndicator.style.setProperty('--indicator-color', priority.color);
|
||
|
||
// Priority value input
|
||
const valueInput = priorityRow.createEl('input', {
|
||
type: 'text',
|
||
value: priority.value,
|
||
cls: 'settings-input value-input settings-view__input settings-view__input--value',
|
||
attr: {
|
||
'aria-label': `Priority value for ${priority.label}`,
|
||
'id': `priority-value-${priority.id}`
|
||
}
|
||
});
|
||
|
||
// Priority label input
|
||
const labelInput = priorityRow.createEl('input', {
|
||
type: 'text',
|
||
value: priority.label,
|
||
cls: 'settings-input label-input settings-view__input settings-view__input--label',
|
||
attr: {
|
||
'aria-label': `Display label for ${priority.label} priority`,
|
||
'id': `priority-label-${priority.id}`
|
||
}
|
||
});
|
||
|
||
// Color input
|
||
const colorInput = priorityRow.createEl('input', {
|
||
type: 'color',
|
||
value: priority.color,
|
||
cls: 'settings-input color-input settings-view__input settings-view__input--color',
|
||
attr: {
|
||
'aria-label': `Color for ${priority.label} priority`,
|
||
'id': `priority-color-${priority.id}`
|
||
}
|
||
});
|
||
|
||
// Weight input
|
||
const weightInput = priorityRow.createEl('input', {
|
||
type: 'number',
|
||
value: priority.weight.toString(),
|
||
cls: 'settings-input weight-input settings-view__input settings-view__input--weight',
|
||
attr: {
|
||
min: '0',
|
||
step: '1',
|
||
'aria-label': `Weight for ${priority.label} priority`,
|
||
'id': `priority-weight-${priority.id}`
|
||
}
|
||
});
|
||
|
||
// Delete button
|
||
const deleteButton = priorityRow.createEl('button', {
|
||
text: 'Delete',
|
||
cls: 'settings-delete-button settings-view__delete-button'
|
||
});
|
||
|
||
// Event listeners
|
||
const updatePriority = async () => {
|
||
try {
|
||
priority.value = valueInput.value;
|
||
priority.label = labelInput.value;
|
||
priority.color = colorInput.value;
|
||
const weightValue = parseInt(weightInput.value);
|
||
if (isNaN(weightValue) || weightValue < 0) {
|
||
new Notice('Priority weight must be a valid positive number.');
|
||
return;
|
||
}
|
||
priority.weight = weightValue;
|
||
await this.plugin.saveSettings();
|
||
colorIndicator.style.setProperty('--indicator-color', priority.color);
|
||
} catch (error) {
|
||
console.error('Error updating priority configuration:', error);
|
||
new Notice('Failed to update priority configuration. Please try again.');
|
||
}
|
||
};
|
||
|
||
valueInput.addEventListener('change', updatePriority);
|
||
labelInput.addEventListener('change', updatePriority);
|
||
colorInput.addEventListener('change', updatePriority);
|
||
weightInput.addEventListener('change', updatePriority);
|
||
|
||
deleteButton.addEventListener('click', async () => {
|
||
if (this.plugin.settings.customPriorities.length <= 1) {
|
||
new Notice('You must have at least 1 priority');
|
||
return;
|
||
}
|
||
|
||
// Show confirmation dialog using Obsidian's Modal API
|
||
const confirmed = await showConfirmationModal(this.app, {
|
||
title: 'Delete Priority',
|
||
message: `Are you sure you want to delete the priority "${priority.label}"?\n\nThis action cannot be undone and may affect existing tasks.`,
|
||
confirmText: 'Delete',
|
||
cancelText: 'Cancel',
|
||
isDestructive: true
|
||
});
|
||
|
||
if (confirmed) {
|
||
const priorityIndex = this.plugin.settings.customPriorities.findIndex(p => p.id === priority.id);
|
||
if (priorityIndex !== -1) {
|
||
this.plugin.settings.customPriorities.splice(priorityIndex, 1);
|
||
await this.plugin.saveSettings();
|
||
this.renderActiveTab();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
private renderPomodoroTab(): void {
|
||
const container = this.tabContents['pomodoro'];
|
||
|
||
new Setting(container)
|
||
.setName('Work duration')
|
||
.setDesc('Duration of work intervals in minutes')
|
||
.addText(text => text
|
||
.setPlaceholder('25')
|
||
.setValue(this.plugin.settings.pomodoroWorkDuration.toString())
|
||
.onChange(async (value) => {
|
||
try {
|
||
const num = parseInt(value);
|
||
if (isNaN(num) || num <= 0) {
|
||
new Notice('Work duration must be a positive number.');
|
||
return;
|
||
}
|
||
this.plugin.settings.pomodoroWorkDuration = num;
|
||
await this.plugin.saveSettings();
|
||
} catch (error) {
|
||
console.error('Error updating pomodoro work duration:', error);
|
||
new Notice('Failed to update work duration setting.');
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Short break duration')
|
||
.setDesc('Duration of short breaks in minutes')
|
||
.addText(text => text
|
||
.setPlaceholder('5')
|
||
.setValue(this.plugin.settings.pomodoroShortBreakDuration.toString())
|
||
.onChange(async (value) => {
|
||
try {
|
||
const num = parseInt(value);
|
||
if (isNaN(num) || num <= 0) {
|
||
new Notice('Short break duration must be a positive number.');
|
||
return;
|
||
}
|
||
this.plugin.settings.pomodoroShortBreakDuration = num;
|
||
await this.plugin.saveSettings();
|
||
} catch (error) {
|
||
console.error('Error updating pomodoro short break duration:', error);
|
||
new Notice('Failed to update short break duration setting.');
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Long break duration')
|
||
.setDesc('Duration of long breaks in minutes')
|
||
.addText(text => text
|
||
.setPlaceholder('15')
|
||
.setValue(this.plugin.settings.pomodoroLongBreakDuration.toString())
|
||
.onChange(async (value) => {
|
||
try {
|
||
const num = parseInt(value);
|
||
if (isNaN(num) || num <= 0) {
|
||
new Notice('Long break duration must be a positive number.');
|
||
return;
|
||
}
|
||
this.plugin.settings.pomodoroLongBreakDuration = num;
|
||
await this.plugin.saveSettings();
|
||
} catch (error) {
|
||
console.error('Error updating pomodoro long break duration:', error);
|
||
new Notice('Failed to update long break duration setting.');
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Long break interval')
|
||
.setDesc('Take a long break after this many pomodoros')
|
||
.addText(text => text
|
||
.setPlaceholder('4')
|
||
.setValue(this.plugin.settings.pomodoroLongBreakInterval.toString())
|
||
.onChange(async (value) => {
|
||
try {
|
||
const num = parseInt(value);
|
||
if (isNaN(num) || num <= 0) {
|
||
new Notice('Long break interval must be a positive number.');
|
||
return;
|
||
}
|
||
this.plugin.settings.pomodoroLongBreakInterval = num;
|
||
await this.plugin.saveSettings();
|
||
} catch (error) {
|
||
console.error('Error updating pomodoro long break interval:', error);
|
||
new Notice('Failed to update long break interval setting.');
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Auto-start breaks')
|
||
.setDesc('Automatically start break timer after work session')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.pomodoroAutoStartBreaks)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.pomodoroAutoStartBreaks = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Auto-start work')
|
||
.setDesc('Automatically start work timer after break')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.pomodoroAutoStartWork)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.pomodoroAutoStartWork = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Enable notifications')
|
||
.setDesc('Show notifications when pomodoro sessions complete')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.pomodoroNotifications)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.pomodoroNotifications = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Enable sound')
|
||
.setDesc('Play sound when pomodoro sessions complete')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.pomodoroSoundEnabled)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.pomodoroSoundEnabled = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Sound volume')
|
||
.setDesc('Volume for completion sounds (0-100)')
|
||
.addSlider(slider => slider
|
||
.setLimits(0, 100, 5)
|
||
.setValue(this.plugin.settings.pomodoroSoundVolume)
|
||
.setDynamicTooltip()
|
||
.onChange(async (value) => {
|
||
try {
|
||
this.plugin.settings.pomodoroSoundVolume = value;
|
||
await this.plugin.saveSettings();
|
||
} catch (error) {
|
||
console.error('Error updating pomodoro sound volume:', error);
|
||
new Notice('Failed to update sound volume setting.');
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Data storage location')
|
||
.setDesc('Choose where to store pomodoro session data. Daily notes provides better data longevity as the data stays with your notes, but requires the Daily Notes core plugin to be enabled.')
|
||
.addDropdown(dropdown => dropdown
|
||
.addOption('plugin', 'Plugin data file (default)')
|
||
.addOption('daily-notes', 'Daily notes frontmatter')
|
||
.setValue(this.plugin.settings.pomodoroStorageLocation)
|
||
.onChange(async (value: 'plugin' | 'daily-notes') => {
|
||
try {
|
||
// Check if Daily Notes plugin is enabled when switching to daily-notes
|
||
if (value === 'daily-notes') {
|
||
const { appHasDailyNotesPluginLoaded } = await import('obsidian-daily-notes-interface');
|
||
if (!appHasDailyNotesPluginLoaded()) {
|
||
new Notice('Daily Notes core plugin must be enabled to use this storage option. Please enable it in Settings > Core plugins and try again.');
|
||
dropdown.setValue('plugin'); // Reset to plugin storage
|
||
return;
|
||
}
|
||
|
||
// Check if there's existing data to migrate
|
||
const data = await this.plugin.loadData();
|
||
const hasExistingData = data?.pomodoroHistory && Array.isArray(data.pomodoroHistory) && data.pomodoroHistory.length > 0;
|
||
|
||
// Show confirmation dialog using Obsidian's Modal API
|
||
const confirmed = await showStorageLocationConfirmationModal(this.app, hasExistingData);
|
||
if (!confirmed) {
|
||
dropdown.setValue('plugin'); // Reset to plugin storage if user cancels
|
||
return;
|
||
}
|
||
}
|
||
|
||
this.plugin.settings.pomodoroStorageLocation = value;
|
||
await this.plugin.saveSettings();
|
||
|
||
// Trigger migration if switching to daily-notes and there's data to migrate
|
||
if (value === 'daily-notes') {
|
||
await this.plugin.pomodoroService.migrateTodailyNotes();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error updating pomodoro storage location:', error);
|
||
new Notice('Failed to update storage location setting.');
|
||
dropdown.setValue('plugin'); // Reset to plugin storage on error
|
||
}
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Auto-stop time tracking on completion')
|
||
.setDesc('Automatically stop time tracking when a task is marked as completed')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.autoStopTimeTrackingOnComplete)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.autoStopTimeTrackingOnComplete = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Show auto-stop notifications')
|
||
.setDesc('Show a notice when time tracking is automatically stopped')
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.plugin.settings.autoStopTimeTrackingNotification)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.autoStopTimeTrackingNotification = value;
|
||
await this.plugin.saveSettings();
|
||
}));
|
||
}
|
||
|
||
|
||
|
||
private renderSubscriptionList(container: HTMLElement): void {
|
||
container.empty();
|
||
|
||
if (!this.plugin.icsSubscriptionService) {
|
||
container.createEl('p', { text: 'ICS Subscription service not available', cls: 'settings-help-note' });
|
||
return;
|
||
}
|
||
|
||
const subscriptions = this.plugin.icsSubscriptionService.getSubscriptions();
|
||
|
||
if (subscriptions.length === 0) {
|
||
container.createEl('p', { text: 'No calendar subscriptions configured', cls: 'settings-help-note' });
|
||
return;
|
||
}
|
||
|
||
subscriptions.forEach(subscription => {
|
||
const subRow = container.createDiv('settings-item-row ics-subscription-row');
|
||
|
||
// Status indicator
|
||
const statusIndicator = subRow.createDiv('settings-status-indicator');
|
||
if (subscription.enabled) {
|
||
statusIndicator.addClass('enabled');
|
||
setTooltip(statusIndicator, subscription.lastError ? `Error: ${subscription.lastError}` : 'Active', { placement: 'top' });
|
||
if (subscription.lastError) {
|
||
statusIndicator.addClass('error');
|
||
}
|
||
} else {
|
||
statusIndicator.addClass('disabled');
|
||
setTooltip(statusIndicator, 'Disabled', { placement: 'top' });
|
||
}
|
||
|
||
// Subscription info
|
||
const infoContainer = subRow.createDiv('ics-subscription-info');
|
||
const nameEl = infoContainer.createEl('div', { cls: 'ics-subscription-name', text: subscription.name });
|
||
|
||
// Type badge
|
||
nameEl.createEl('span', {
|
||
cls: `ics-subscription-type-badge ${subscription.type}`,
|
||
text: subscription.type === 'remote' ? 'URL' : 'FILE'
|
||
});
|
||
|
||
// Source (URL or file path)
|
||
const sourceText = subscription.type === 'remote' ? subscription.url : subscription.filePath;
|
||
infoContainer.createEl('div', { cls: 'ics-subscription-url', text: sourceText || 'Unknown source' });
|
||
const metaEl = infoContainer.createEl('div', { cls: 'ics-subscription-meta' });
|
||
|
||
// Meta information
|
||
const refreshText = `Refresh: ${subscription.refreshInterval}min`;
|
||
const lastFetched = subscription.lastFetched ? ` • Last: ${new Date(subscription.lastFetched).toLocaleString()}` : '';
|
||
metaEl.textContent = refreshText + lastFetched;
|
||
|
||
if (subscription.lastError) {
|
||
infoContainer.createEl('div', { cls: 'ics-subscription-error', text: `Error: ${subscription.lastError}` });
|
||
}
|
||
|
||
// Actions
|
||
const actionsContainer = subRow.createDiv('ics-subscription-actions');
|
||
|
||
// Enable/disable toggle
|
||
const enableButton = actionsContainer.createEl('button', {
|
||
text: subscription.enabled ? 'Disable' : 'Enable',
|
||
cls: `ics-subscription-toggle ${subscription.enabled ? 'enabled' : 'disabled'}`
|
||
});
|
||
enableButton.addEventListener('click', async () => {
|
||
try {
|
||
await this.plugin.icsSubscriptionService!.updateSubscription(subscription.id, {
|
||
enabled: !subscription.enabled
|
||
});
|
||
this.renderActiveTab();
|
||
} catch (error) {
|
||
console.error('Error toggling subscription:', error);
|
||
new Notice('Failed to update subscription');
|
||
}
|
||
});
|
||
|
||
// Refresh button
|
||
const refreshButton = actionsContainer.createEl('button', {
|
||
text: 'Refresh',
|
||
cls: 'ics-subscription-refresh'
|
||
});
|
||
refreshButton.addEventListener('click', async () => {
|
||
if (!subscription.enabled) {
|
||
new Notice('Enable the subscription first');
|
||
return;
|
||
}
|
||
|
||
refreshButton.textContent = 'Refreshing...';
|
||
refreshButton.disabled = true;
|
||
try {
|
||
await this.plugin.icsSubscriptionService!.refreshSubscription(subscription.id);
|
||
new Notice(`Refreshed "${subscription.name}"`);
|
||
this.renderActiveTab();
|
||
} catch (error) {
|
||
console.error('Error refreshing subscription:', error);
|
||
new Notice('Failed to refresh subscription');
|
||
} finally {
|
||
refreshButton.textContent = 'Refresh';
|
||
refreshButton.disabled = false;
|
||
}
|
||
});
|
||
|
||
// Edit button
|
||
const editButton = actionsContainer.createEl('button', {
|
||
text: 'Edit',
|
||
cls: 'ics-subscription-edit'
|
||
});
|
||
editButton.addEventListener('click', () => {
|
||
this.showInlineEditForm(subscription, subRow);
|
||
});
|
||
|
||
// Delete button
|
||
const deleteButton = actionsContainer.createEl('button', {
|
||
text: 'Delete',
|
||
cls: 'ics-subscription-delete'
|
||
});
|
||
deleteButton.addEventListener('click', async () => {
|
||
// Show confirmation dialog using the existing, correct modal
|
||
const confirmed = await showConfirmationModal(this.app, {
|
||
title: 'Delete Subscription',
|
||
message: `Are you sure you want to delete the subscription "${subscription.name}"? This action cannot be undone.`,
|
||
confirmText: 'Delete',
|
||
cancelText: 'Cancel',
|
||
isDestructive: true
|
||
});
|
||
|
||
if (confirmed) {
|
||
try {
|
||
await this.plugin.icsSubscriptionService!.removeSubscription(subscription.id);
|
||
new Notice(`Deleted subscription "${subscription.name}"`);
|
||
this.renderActiveTab();
|
||
} catch (error) {
|
||
console.error('Error deleting subscription:', error);
|
||
new Notice('Failed to delete subscription');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
private showInlineEditForm(subscription: any, rowElement: HTMLElement): void {
|
||
// Store reference to original row content for restoration
|
||
const originalChildren = Array.from(rowElement.children);
|
||
|
||
// Clear the row and create edit form
|
||
rowElement.empty();
|
||
rowElement.addClass('ics-subscription-editing');
|
||
|
||
const editForm = rowElement.createDiv('ics-edit-form');
|
||
|
||
// Name input
|
||
const nameRow = editForm.createDiv('ics-edit-row');
|
||
nameRow.createEl('label', { text: 'Name:', cls: 'ics-edit-label' });
|
||
const nameInput = nameRow.createEl('input', {
|
||
type: 'text',
|
||
value: subscription.name,
|
||
cls: 'ics-edit-input'
|
||
});
|
||
|
||
// URL input
|
||
const urlRow = editForm.createDiv('ics-edit-row');
|
||
urlRow.createEl('label', { text: 'URL:', cls: 'ics-edit-label' });
|
||
const urlInput = urlRow.createEl('input', {
|
||
type: 'url',
|
||
value: subscription.url,
|
||
cls: 'ics-edit-input'
|
||
});
|
||
|
||
// Settings row
|
||
const settingsRow = editForm.createDiv('ics-edit-row ics-edit-settings');
|
||
|
||
// Color
|
||
const colorGroup = settingsRow.createDiv('ics-edit-group');
|
||
colorGroup.createEl('label', { text: 'Color:', cls: 'ics-edit-label' });
|
||
const colorInput = colorGroup.createEl('input', {
|
||
type: 'color',
|
||
value: subscription.color,
|
||
cls: 'ics-edit-color'
|
||
});
|
||
|
||
// Refresh interval
|
||
const intervalGroup = settingsRow.createDiv('ics-edit-group');
|
||
intervalGroup.createEl('label', { text: 'Refresh (min):', cls: 'ics-edit-label' });
|
||
const intervalInput = intervalGroup.createEl('input', {
|
||
type: 'number',
|
||
value: subscription.refreshInterval.toString(),
|
||
cls: 'ics-edit-number'
|
||
});
|
||
intervalInput.setAttribute('min', '15');
|
||
intervalInput.setAttribute('max', '1440');
|
||
intervalInput.setAttribute('step', '15');
|
||
|
||
// Enabled checkbox
|
||
const enabledGroup = settingsRow.createDiv('ics-edit-group');
|
||
const enabledLabel = enabledGroup.createEl('label', { cls: 'ics-edit-checkbox-label' });
|
||
const enabledCheckbox = enabledLabel.createEl('input', {
|
||
type: 'checkbox',
|
||
cls: 'ics-edit-checkbox'
|
||
});
|
||
enabledCheckbox.checked = subscription.enabled;
|
||
enabledLabel.createSpan({ text: ' Enabled' });
|
||
|
||
// Buttons row
|
||
const buttonsRow = editForm.createDiv('ics-edit-row ics-edit-buttons');
|
||
const saveButton = buttonsRow.createEl('button', {
|
||
text: 'Save',
|
||
cls: 'ics-edit-button mod-cta'
|
||
});
|
||
const cancelButton = buttonsRow.createEl('button', {
|
||
text: 'Cancel',
|
||
cls: 'ics-edit-button'
|
||
});
|
||
|
||
// Save handler
|
||
saveButton.addEventListener('click', async () => {
|
||
const name = nameInput.value.trim();
|
||
const url = urlInput.value.trim();
|
||
const color = colorInput.value;
|
||
const refreshInterval = parseInt(intervalInput.value);
|
||
const enabled = enabledCheckbox.checked;
|
||
|
||
if (!name || !url) {
|
||
new Notice('Name and URL are required');
|
||
return;
|
||
}
|
||
|
||
if (refreshInterval < 15 || refreshInterval > 1440) {
|
||
new Notice('Refresh interval must be between 15 and 1440 minutes');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
saveButton.textContent = 'Saving...';
|
||
saveButton.disabled = true;
|
||
|
||
await this.plugin.icsSubscriptionService!.updateSubscription(subscription.id, {
|
||
name, url, color, refreshInterval, enabled
|
||
});
|
||
|
||
new Notice(`Updated subscription "${name}"`);
|
||
this.renderActiveTab();
|
||
} catch (error) {
|
||
console.error('Error updating subscription:', error);
|
||
new Notice('Failed to update subscription');
|
||
}
|
||
});
|
||
|
||
// Cancel handler
|
||
cancelButton.addEventListener('click', () => {
|
||
rowElement.empty();
|
||
rowElement.removeClass('ics-subscription-editing');
|
||
// Restore original children
|
||
originalChildren.forEach(child => rowElement.appendChild(child));
|
||
// Re-attach event listeners by re-rendering
|
||
this.renderActiveTab();
|
||
});
|
||
|
||
// Focus the name input
|
||
window.setTimeout(() => nameInput.focus(), 50);
|
||
}
|
||
|
||
private initializeDefaultProjectsFromSettings(): void {
|
||
// Convert project strings to files
|
||
const defaultProjects = this.plugin.settings.taskCreationDefaults.defaultProjects;
|
||
if (!defaultProjects) {
|
||
this.selectedDefaultProjectFiles = [];
|
||
return;
|
||
}
|
||
|
||
const projectStrings = defaultProjects.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
||
this.selectedDefaultProjectFiles = [];
|
||
|
||
for (const projectString of projectStrings) {
|
||
// Check if it's a wiki link format
|
||
const linkMatch = projectString.match(/^\[\[([^\]]+)\]\]$/);
|
||
if (linkMatch) {
|
||
const linkPath = linkMatch[1];
|
||
const file = this.app.metadataCache.getFirstLinkpathDest(linkPath, '');
|
||
if (file) {
|
||
this.selectedDefaultProjectFiles.push(file);
|
||
}
|
||
} else {
|
||
// For backwards compatibility, try to find a file with this name
|
||
const files = this.app.vault.getMarkdownFiles();
|
||
const matchingFile = files.find(f =>
|
||
f.basename === projectString ||
|
||
f.name === projectString + '.md'
|
||
);
|
||
if (matchingFile) {
|
||
this.selectedDefaultProjectFiles.push(matchingFile);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private addDefaultProject(file: TAbstractFile): void {
|
||
// Check if project is already selected
|
||
const exists = this.selectedDefaultProjectFiles.some(
|
||
existing => existing.path === file.path
|
||
);
|
||
if (!exists) {
|
||
this.selectedDefaultProjectFiles.push(file);
|
||
}
|
||
}
|
||
|
||
private removeDefaultProject(file: TAbstractFile): void {
|
||
this.selectedDefaultProjectFiles = this.selectedDefaultProjectFiles.filter(
|
||
existing => existing.path !== file.path
|
||
);
|
||
}
|
||
|
||
private renderDefaultProjectsList(container: HTMLElement): void {
|
||
container.empty();
|
||
|
||
if (this.selectedDefaultProjectFiles.length === 0) {
|
||
const emptyText = container.createDiv({ cls: 'default-projects-empty' });
|
||
emptyText.textContent = 'No default projects selected';
|
||
return;
|
||
}
|
||
|
||
this.selectedDefaultProjectFiles.forEach(file => {
|
||
const projectItem = container.createDiv({ cls: 'default-project-item' });
|
||
|
||
// Info container
|
||
const infoEl = projectItem.createDiv({ cls: 'default-project-info' });
|
||
|
||
// File name
|
||
const nameEl = infoEl.createSpan({ cls: 'default-project-name' });
|
||
nameEl.textContent = file.name;
|
||
|
||
// File path (if different from name)
|
||
if (file.path !== file.name) {
|
||
const pathEl = infoEl.createDiv({ cls: 'default-project-path' });
|
||
pathEl.textContent = file.path;
|
||
}
|
||
|
||
// Remove button
|
||
const removeBtn = projectItem.createEl('button', {
|
||
cls: 'default-project-remove',
|
||
text: '×'
|
||
});
|
||
setTooltip(removeBtn, 'Remove project', { placement: 'top' });
|
||
removeBtn.addEventListener('click', () => {
|
||
this.removeDefaultProject(file);
|
||
this.renderDefaultProjectsList(container);
|
||
this.updateDefaultProjectsInSettings();
|
||
});
|
||
});
|
||
}
|
||
|
||
private updateDefaultProjectsInSettings(): void {
|
||
// Convert selected files to markdown links
|
||
const currentFile = this.app.workspace.getActiveFile();
|
||
const sourcePath = currentFile?.path || '';
|
||
|
||
const projectStrings = this.selectedDefaultProjectFiles.map(file => {
|
||
// fileToLinktext expects TFile, so cast safely since we know these are markdown files
|
||
const linkText = this.app.metadataCache.fileToLinktext(file as TFile, sourcePath, true);
|
||
return `[[${linkText}]]`;
|
||
});
|
||
|
||
this.plugin.settings.taskCreationDefaults.defaultProjects = projectStrings.join(', ');
|
||
this.plugin.saveSettings();
|
||
}
|
||
|
||
private renderDefaultRemindersList(container: HTMLElement): void {
|
||
container.empty();
|
||
|
||
const reminders = this.plugin.settings.taskCreationDefaults.defaultReminders || [];
|
||
|
||
if (reminders.length === 0) {
|
||
const emptyState = container.createDiv({ cls: 'reminder-defaults-empty' });
|
||
setIcon(emptyState.createDiv({ cls: 'reminder-defaults-empty-icon' }), 'bell-off');
|
||
emptyState.createEl('div', {
|
||
cls: 'reminder-defaults-empty-text',
|
||
text: 'No default reminders configured'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const remindersList = container.createDiv({ cls: 'reminder-defaults-items' });
|
||
|
||
reminders.forEach((reminder, index) => {
|
||
const reminderCard = remindersList.createDiv({ cls: 'reminder-defaults-card' });
|
||
|
||
// Reminder type icon
|
||
const iconContainer = reminderCard.createDiv({ cls: 'reminder-defaults-icon' });
|
||
const iconName = reminder.type === 'absolute' ? 'calendar-clock' : 'timer';
|
||
setIcon(iconContainer, iconName);
|
||
|
||
// Main content area
|
||
const content = reminderCard.createDiv({ cls: 'reminder-defaults-content' });
|
||
|
||
// Primary info (timing)
|
||
const primaryInfo = content.createDiv({ cls: 'reminder-defaults-primary' });
|
||
primaryInfo.textContent = this.formatDefaultReminderText(reminder);
|
||
|
||
// Custom description (if any)
|
||
if (reminder.description) {
|
||
const description = content.createDiv({ cls: 'reminder-defaults-description' });
|
||
description.textContent = `"${reminder.description}"`;
|
||
}
|
||
|
||
// Actions area
|
||
const actions = reminderCard.createDiv({ cls: 'reminder-defaults-actions' });
|
||
|
||
// Remove button
|
||
const removeBtn = actions.createEl('button', {
|
||
cls: 'reminder-defaults-remove-btn'
|
||
});
|
||
setIcon(removeBtn, 'trash-2');
|
||
setTooltip(removeBtn, 'Delete this default reminder');
|
||
removeBtn.onclick = async () => {
|
||
await this.removeDefaultReminder(index);
|
||
};
|
||
});
|
||
}
|
||
|
||
private renderAddDefaultReminderForm(container: HTMLElement): void {
|
||
const formContainer = container.createDiv({ cls: 'reminder-defaults-form' });
|
||
|
||
const formHeader = formContainer.createEl('h4', {
|
||
text: 'Add Default Reminder',
|
||
cls: 'reminder-defaults-form-header'
|
||
});
|
||
|
||
// Type selector
|
||
const typeSelector = formContainer.createDiv({ cls: 'reminder-defaults-type-selector' });
|
||
|
||
const relativeTab = typeSelector.createEl('button', {
|
||
cls: 'reminder-defaults-type-tab reminder-defaults-type-tab--active',
|
||
text: 'Relative',
|
||
attr: { 'data-type': 'relative' }
|
||
});
|
||
|
||
const absoluteTab = typeSelector.createEl('button', {
|
||
cls: 'reminder-defaults-type-tab',
|
||
text: 'Absolute',
|
||
attr: { 'data-type': 'absolute' }
|
||
});
|
||
|
||
let selectedType: 'relative' | 'absolute' = 'relative';
|
||
let relativeAnchor: 'due' | 'scheduled' = 'due';
|
||
let relativeOffset = 15;
|
||
let relativeUnit: 'minutes' | 'hours' | 'days' = 'minutes';
|
||
let relativeDirection: 'before' | 'after' = 'before';
|
||
let absoluteDate = '';
|
||
let absoluteTime = '';
|
||
let description = '';
|
||
|
||
// Tab switching
|
||
const switchToType = (type: 'relative' | 'absolute') => {
|
||
selectedType = type;
|
||
relativeTab.classList.toggle('reminder-defaults-type-tab--active', type === 'relative');
|
||
absoluteTab.classList.toggle('reminder-defaults-type-tab--active', type === 'absolute');
|
||
updateFormVisibility();
|
||
};
|
||
|
||
relativeTab.onclick = () => switchToType('relative');
|
||
absoluteTab.onclick = () => switchToType('absolute');
|
||
|
||
// Relative form fields
|
||
const relativeFields = formContainer.createDiv({ cls: 'reminder-defaults-relative-fields' });
|
||
|
||
new Setting(relativeFields)
|
||
.setName('Time')
|
||
.addText(text => {
|
||
text
|
||
.setPlaceholder('15')
|
||
.setValue(String(relativeOffset))
|
||
.onChange(value => {
|
||
relativeOffset = parseInt(value) || 0;
|
||
});
|
||
})
|
||
.addDropdown(dropdown => {
|
||
dropdown
|
||
.addOption('minutes', 'minutes')
|
||
.addOption('hours', 'hours')
|
||
.addOption('days', 'days')
|
||
.setValue(relativeUnit)
|
||
.onChange(value => {
|
||
relativeUnit = value as 'minutes' | 'hours' | 'days';
|
||
});
|
||
});
|
||
|
||
new Setting(relativeFields)
|
||
.setName('Direction')
|
||
.addDropdown(dropdown => {
|
||
dropdown
|
||
.addOption('before', 'Before')
|
||
.addOption('after', 'After')
|
||
.setValue(relativeDirection)
|
||
.onChange(value => {
|
||
relativeDirection = value as 'before' | 'after';
|
||
});
|
||
});
|
||
|
||
new Setting(relativeFields)
|
||
.setName('Relative to')
|
||
.addDropdown(dropdown => {
|
||
dropdown
|
||
.addOption('due', 'Due date')
|
||
.addOption('scheduled', 'Scheduled date')
|
||
.setValue(relativeAnchor)
|
||
.onChange(value => {
|
||
relativeAnchor = value as 'due' | 'scheduled';
|
||
});
|
||
});
|
||
|
||
// Absolute form fields
|
||
const absoluteFields = formContainer.createDiv({ cls: 'reminder-defaults-absolute-fields' });
|
||
|
||
new Setting(absoluteFields)
|
||
.setName('Date')
|
||
.addText(text => {
|
||
text
|
||
.setPlaceholder('YYYY-MM-DD')
|
||
.setValue(absoluteDate)
|
||
.onChange(value => {
|
||
absoluteDate = value;
|
||
});
|
||
text.inputEl.type = 'date';
|
||
});
|
||
|
||
new Setting(absoluteFields)
|
||
.setName('Time')
|
||
.addText(text => {
|
||
text
|
||
.setPlaceholder('HH:MM')
|
||
.setValue(absoluteTime)
|
||
.onChange(value => {
|
||
absoluteTime = value;
|
||
});
|
||
text.inputEl.type = 'time';
|
||
});
|
||
|
||
// Description field (common)
|
||
new Setting(formContainer)
|
||
.setName('Description (optional)')
|
||
.addText(text => {
|
||
text
|
||
.setPlaceholder('Custom reminder message')
|
||
.setValue(description)
|
||
.onChange(value => {
|
||
description = value;
|
||
});
|
||
});
|
||
|
||
// Add button
|
||
const addBtn = formContainer.createEl('button', {
|
||
cls: 'reminder-defaults-add-btn'
|
||
});
|
||
|
||
const addIcon = addBtn.createSpan({ cls: 'reminder-defaults-add-icon' });
|
||
setIcon(addIcon, 'plus');
|
||
addBtn.createSpan({
|
||
cls: 'reminder-defaults-add-text',
|
||
text: 'Add Default Reminder'
|
||
});
|
||
|
||
addBtn.onclick = async () => {
|
||
try {
|
||
const newReminder = this.createDefaultReminder(
|
||
selectedType,
|
||
relativeAnchor,
|
||
relativeOffset,
|
||
relativeUnit,
|
||
relativeDirection,
|
||
absoluteDate,
|
||
absoluteTime,
|
||
description
|
||
);
|
||
|
||
if (newReminder) {
|
||
await this.addDefaultReminder(newReminder);
|
||
|
||
// Reset form
|
||
if (selectedType === 'relative') {
|
||
relativeOffset = 15;
|
||
relativeUnit = 'minutes';
|
||
description = '';
|
||
} else {
|
||
absoluteDate = '';
|
||
absoluteTime = '';
|
||
description = '';
|
||
}
|
||
|
||
// Reset form inputs
|
||
this.resetDefaultReminderForm(formContainer);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding default reminder:', error);
|
||
new Notice('Failed to add default reminder. Please check your inputs.');
|
||
}
|
||
};
|
||
|
||
const updateFormVisibility = () => {
|
||
relativeFields.style.display = selectedType === 'relative' ? 'block' : 'none';
|
||
absoluteFields.style.display = selectedType === 'absolute' ? 'block' : 'none';
|
||
};
|
||
|
||
// Set initial form visibility
|
||
updateFormVisibility();
|
||
}
|
||
|
||
private formatDefaultReminderText(reminder: DefaultReminder): string {
|
||
if (reminder.type === 'absolute') {
|
||
if (reminder.absoluteDate && reminder.absoluteTime) {
|
||
return `${reminder.absoluteDate} at ${reminder.absoluteTime}`;
|
||
}
|
||
return 'Absolute reminder';
|
||
} else {
|
||
const anchor = reminder.relatedTo === 'due' ? 'due date' : 'scheduled date';
|
||
const offset = this.formatDefaultReminderOffset(reminder);
|
||
return `${offset} ${anchor}`;
|
||
}
|
||
}
|
||
|
||
private formatDefaultReminderOffset(reminder: DefaultReminder): string {
|
||
if (!reminder.offset || !reminder.unit) return 'At time of';
|
||
|
||
const direction = reminder.direction === 'before' ? 'before' : 'after';
|
||
const unit = reminder.offset === 1 ? reminder.unit.slice(0, -1) : reminder.unit; // Remove 's' for singular
|
||
return `${reminder.offset} ${unit} ${direction}`;
|
||
}
|
||
|
||
private createDefaultReminder(
|
||
type: 'relative' | 'absolute',
|
||
anchor: 'due' | 'scheduled',
|
||
offset: number,
|
||
unit: 'minutes' | 'hours' | 'days',
|
||
direction: 'before' | 'after',
|
||
date: string,
|
||
time: string,
|
||
description: string
|
||
): DefaultReminder | null {
|
||
const id = `def_rem_${Date.now()}`;
|
||
|
||
if (type === 'relative') {
|
||
return {
|
||
id,
|
||
type: 'relative',
|
||
relatedTo: anchor,
|
||
offset,
|
||
unit,
|
||
direction,
|
||
description: description || undefined
|
||
};
|
||
} else {
|
||
if (!date || !time) {
|
||
new Notice('Please specify both date and time for absolute reminder');
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
id,
|
||
type: 'absolute',
|
||
absoluteDate: date,
|
||
absoluteTime: time,
|
||
description: description || undefined
|
||
};
|
||
}
|
||
}
|
||
|
||
private async addDefaultReminder(reminder: DefaultReminder): Promise<void> {
|
||
if (!this.plugin.settings.taskCreationDefaults.defaultReminders) {
|
||
this.plugin.settings.taskCreationDefaults.defaultReminders = [];
|
||
}
|
||
|
||
this.plugin.settings.taskCreationDefaults.defaultReminders.push(reminder);
|
||
await this.plugin.saveSettings();
|
||
|
||
// Re-render the list
|
||
const remindersList = document.querySelector('.reminder-defaults-list') as HTMLElement;
|
||
if (remindersList) {
|
||
this.renderDefaultRemindersList(remindersList);
|
||
}
|
||
|
||
new Notice('Default reminder added successfully');
|
||
}
|
||
|
||
private async removeDefaultReminder(index: number): Promise<void> {
|
||
if (!this.plugin.settings.taskCreationDefaults.defaultReminders) return;
|
||
|
||
this.plugin.settings.taskCreationDefaults.defaultReminders.splice(index, 1);
|
||
await this.plugin.saveSettings();
|
||
|
||
// Re-render the list
|
||
const remindersList = document.querySelector('.reminder-defaults-list') as HTMLElement;
|
||
if (remindersList) {
|
||
this.renderDefaultRemindersList(remindersList);
|
||
}
|
||
|
||
new Notice('Default reminder removed');
|
||
}
|
||
|
||
private resetDefaultReminderForm(formContainer: HTMLElement): void {
|
||
// Reset all form inputs to their default values
|
||
const inputs = formContainer.querySelectorAll('input, select') as NodeListOf<HTMLInputElement | HTMLSelectElement>;
|
||
inputs.forEach(input => {
|
||
if (input.type === 'text' || input.type === 'date' || input.type === 'time') {
|
||
input.value = '';
|
||
} else if (input.tagName === 'SELECT') {
|
||
(input as HTMLSelectElement).selectedIndex = 0;
|
||
}
|
||
});
|
||
|
||
// Reset number input to default
|
||
const offsetInput = formContainer.querySelector('input[placeholder="15"]') as HTMLInputElement;
|
||
if (offsetInput) offsetInput.value = '15';
|
||
}
|
||
|
||
/**
|
||
* Render the list of configured webhooks
|
||
*/
|
||
private renderWebhookList(container: HTMLElement): void {
|
||
const webhooksContainer = container.createDiv({ cls: 'webhooks-container' });
|
||
|
||
if (!this.plugin.settings.webhooks || this.plugin.settings.webhooks.length === 0) {
|
||
const emptyState = webhooksContainer.createDiv({ cls: 'setting-item-description' });
|
||
emptyState.textContent = 'No webhooks configured. Add a webhook to receive real-time notifications.';
|
||
return;
|
||
}
|
||
|
||
this.plugin.settings.webhooks.forEach((webhook, index) => {
|
||
const webhookItem = webhooksContainer.createDiv({ cls: 'webhook-item' });
|
||
|
||
const webhookInfo = webhookItem.createDiv({ cls: 'webhook-info' });
|
||
webhookInfo.createEl('strong', { text: webhook.url });
|
||
|
||
const eventsList = webhookInfo.createDiv({ cls: 'webhook-events' });
|
||
eventsList.textContent = `Events: ${webhook.events.join(', ')}`;
|
||
|
||
const webhookStatus = webhookInfo.createDiv({ cls: 'webhook-status' });
|
||
webhookStatus.innerHTML = `
|
||
<span class="webhook-status-indicator ${webhook.active ? 'active' : 'inactive'}">
|
||
${webhook.active ? 'Active' : 'Inactive'}
|
||
</span>
|
||
<span class="webhook-stats">
|
||
✓ ${webhook.successCount || 0} | ✗ ${webhook.failureCount || 0}
|
||
</span>
|
||
`;
|
||
|
||
const webhookActions = webhookItem.createDiv({ cls: 'webhook-actions' });
|
||
|
||
// Toggle active/inactive
|
||
const toggleBtn = webhookActions.createEl('button', {
|
||
text: webhook.active ? 'Disable' : 'Enable',
|
||
cls: 'mod-cta'
|
||
});
|
||
toggleBtn.onclick = async () => {
|
||
webhook.active = !webhook.active;
|
||
await this.plugin.saveSettings();
|
||
this.display(); // Refresh the settings
|
||
new Notice(`Webhook ${webhook.active ? 'enabled' : 'disabled'}`);
|
||
};
|
||
|
||
// Delete webhook
|
||
const deleteBtn = webhookActions.createEl('button', {
|
||
text: 'Delete',
|
||
cls: 'mod-warning'
|
||
});
|
||
deleteBtn.onclick = async () => {
|
||
const confirmed = await showConfirmationModal(this.app, {
|
||
title: 'Delete Webhook',
|
||
message: `Are you sure you want to delete this webhook?\n\nURL: ${webhook.url}\n\nThis action cannot be undone.`,
|
||
confirmText: 'Delete',
|
||
cancelText: 'Cancel',
|
||
isDestructive: true
|
||
});
|
||
|
||
if (confirmed) {
|
||
this.plugin.settings.webhooks.splice(index, 1);
|
||
await this.plugin.saveSettings();
|
||
this.display(); // Refresh the settings
|
||
new Notice('Webhook deleted');
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show modal for adding a new webhook
|
||
*/
|
||
private showWebhookModal(): void {
|
||
const modal = new WebhookModal(this.app, async (webhookConfig: Partial<WebhookConfig>) => {
|
||
// Generate ID and secret
|
||
const webhook: WebhookConfig = {
|
||
id: `wh_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||
url: webhookConfig.url || '',
|
||
events: webhookConfig.events || [],
|
||
secret: this.generateWebhookSecret(),
|
||
active: true,
|
||
createdAt: new Date().toISOString(),
|
||
failureCount: 0,
|
||
successCount: 0
|
||
};
|
||
|
||
if (!this.plugin.settings.webhooks) {
|
||
this.plugin.settings.webhooks = [];
|
||
}
|
||
|
||
this.plugin.settings.webhooks.push(webhook);
|
||
await this.plugin.saveSettings();
|
||
this.display(); // Refresh settings
|
||
|
||
new Notice(`Webhook added successfully!\n\nSecret: ${webhook.secret.substring(0, 8)}...`);
|
||
});
|
||
|
||
modal.open();
|
||
}
|
||
|
||
/**
|
||
* Generate secure webhook secret
|
||
*/
|
||
private generateWebhookSecret(): string {
|
||
return Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||
.map(b => b.toString(16).padStart(2, '0'))
|
||
.join('');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Modal for adding/editing webhooks
|
||
*/
|
||
class WebhookModal extends Modal {
|
||
private url = '';
|
||
private selectedEvents: string[] = [];
|
||
private onSubmit: (config: Partial<WebhookConfig>) => void;
|
||
|
||
constructor(app: App, onSubmit: (config: Partial<WebhookConfig>) => void) {
|
||
super(app);
|
||
this.onSubmit = onSubmit;
|
||
}
|
||
|
||
onOpen(): void {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
|
||
contentEl.createEl('h2', { text: 'Add Webhook' });
|
||
|
||
// URL input
|
||
new Setting(contentEl)
|
||
.setName('Webhook URL')
|
||
.setDesc('The endpoint where webhook payloads will be sent')
|
||
.addText(text => text
|
||
.setPlaceholder('https://your-service.com/webhook')
|
||
.setValue(this.url)
|
||
.onChange((value) => {
|
||
this.url = value;
|
||
}));
|
||
|
||
// Events selection
|
||
const eventsContainer = contentEl.createDiv();
|
||
eventsContainer.createEl('h3', { text: 'Events to subscribe to:' });
|
||
|
||
const availableEvents = [
|
||
'task.created', 'task.updated', 'task.deleted', 'task.completed',
|
||
'task.archived', 'task.unarchived', 'time.started', 'time.stopped',
|
||
'pomodoro.started', 'pomodoro.completed', 'pomodoro.interrupted',
|
||
'recurring.instance.completed'
|
||
];
|
||
|
||
availableEvents.forEach(event => {
|
||
const eventSetting = new Setting(eventsContainer)
|
||
.setName(event)
|
||
.setDesc(`Subscribe to ${event} events`)
|
||
.addToggle(toggle => toggle
|
||
.setValue(this.selectedEvents.includes(event))
|
||
.onChange((value) => {
|
||
if (value) {
|
||
this.selectedEvents.push(event);
|
||
} else {
|
||
const index = this.selectedEvents.indexOf(event);
|
||
if (index > -1) {
|
||
this.selectedEvents.splice(index, 1);
|
||
}
|
||
}
|
||
}));
|
||
});
|
||
|
||
// Buttons
|
||
const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' });
|
||
|
||
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
|
||
cancelBtn.onclick = () => this.close();
|
||
|
||
const saveBtn = buttonContainer.createEl('button', {
|
||
text: 'Add Webhook',
|
||
cls: 'mod-cta'
|
||
});
|
||
saveBtn.onclick = () => {
|
||
if (!this.url.trim()) {
|
||
new Notice('Webhook URL is required');
|
||
return;
|
||
}
|
||
|
||
if (this.selectedEvents.length === 0) {
|
||
new Notice('Please select at least one event');
|
||
return;
|
||
}
|
||
|
||
this.onSubmit({
|
||
url: this.url.trim(),
|
||
events: this.selectedEvents as any[]
|
||
});
|
||
|
||
this.close();
|
||
};
|
||
}
|
||
|
||
onClose(): void {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
}
|
||
}
|