diff --git a/src/services/CalendarService.ts b/src/services/CalendarService.ts index 46e2931..74c665b 100644 --- a/src/services/CalendarService.ts +++ b/src/services/CalendarService.ts @@ -13,6 +13,8 @@ export interface CalendarEvent { description?: string; location?: string; source: string; + sourceId: string; // Unique identifier for the calendar source + color?: string; // Color assigned to this event's calendar } interface CacheData { @@ -474,6 +476,8 @@ export class CalendarService { description: event.description, location: event.location, source: source.name, + sourceId: source.url, // Using URL as unique identifier + color: source.color, }); } } @@ -497,6 +501,8 @@ export class CalendarService { description: event.description, location: event.location, source: source.name, + sourceId: source.url, // Using URL as unique identifier + color: source.color, }]; } @@ -567,6 +573,8 @@ export class CalendarService { description: exception.description, location: exception.location, source: source.name, + sourceId: source.url, // Using URL as unique identifier + color: source.color, }; } diff --git a/src/services/IcsImportService.ts b/src/services/IcsImportService.ts index bec6183..c58ad3a 100644 --- a/src/services/IcsImportService.ts +++ b/src/services/IcsImportService.ts @@ -39,7 +39,8 @@ export class IcsImportService { end: endDate, description: event.description, location: event.location, - source: "Imported" + source: "Imported", + sourceId: "imported" // Special source ID for imported events }; } catch (error) { if (error instanceof Error) { diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 8e78847..6886e5a 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -13,6 +13,7 @@ import { } from "obsidian"; import MemoChron from "../main"; import { CalendarSource } from "./types"; +import { CALENDAR_COLOR_PALETTE } from "../utils/constants"; export class SettingsTab extends PluginSettingTab { constructor( @@ -50,6 +51,7 @@ export class SettingsTab extends PluginSettingTab { private renderGeneralSection(): void { this.renderFirstDayOfWeek(); this.renderHideCalendar(); + this.renderEnableCalendarColors(); this.renderRefreshInterval(); } @@ -72,26 +74,57 @@ export class SettingsTab extends PluginSettingTab { } private async addNewCalendar(): Promise { - this.plugin.settings.calendarUrls.push({ + const newCalendar: CalendarSource = { url: "", name: "New calendar", enabled: true, tags: [], - }); + }; + + // Auto-assign color if colors are enabled + if (this.plugin.settings.enableCalendarColors) { + newCalendar.color = this.getNextAvailableColor(); + } + + this.plugin.settings.calendarUrls.push(newCalendar); await this.plugin.saveSettings(); this.display(); } + private getNextAvailableColor(): string { + const usedColors = this.plugin.settings.calendarUrls + .map(source => source.color) + .filter(color => color); + + // Find the first unused color in the palette + for (const color of CALENDAR_COLOR_PALETTE) { + if (!usedColors.includes(color)) { + return color; + } + } + + // If all colors are used, cycle back to the beginning + const index = this.plugin.settings.calendarUrls.length % CALENDAR_COLOR_PALETTE.length; + return CALENDAR_COLOR_PALETTE[index]; + } + private renderCalendarSource( container: HTMLElement, source: CalendarSource, index: number ): void { - new Setting(container) + const setting = new Setting(container) .addText((text) => this.setupUrlInput(text, source, index)) .addButton((btn) => this.setupFilePickerButton(btn, index)) .addText((text) => this.setupNameInput(text, source, index)) - .addText((text) => this.setupTagsInput(text, source, index)) + .addText((text) => this.setupTagsInput(text, source, index)); + + // Add color picker if colors are enabled + if (this.plugin.settings.enableCalendarColors) { + setting.addButton((btn) => this.setupColorPicker(btn, source, index)); + } + + setting .addToggle((toggle) => this.setupEnabledToggle(toggle, source, index)) .addButton((btn) => this.setupRemoveButton(btn, index)); } @@ -184,6 +217,42 @@ export class SettingsTab extends PluginSettingTab { }); } + private setupColorPicker( + btn: ButtonComponent, + source: CalendarSource, + index: number + ): ButtonComponent { + const currentColor = source.color || CALENDAR_COLOR_PALETTE[index % CALENDAR_COLOR_PALETTE.length]; + + return btn + .setButtonText("Color") + .setTooltip("Choose calendar color") + .onClick(() => { + this.showColorPicker(source, index, btn); + }) + .then((button) => { + // Add a visual color indicator + this.updateColorButton(button.buttonEl, currentColor); + return button; + }); + } + + private updateColorButton(buttonEl: HTMLElement, color: string) { + buttonEl.style.setProperty("--selected-color", color); + buttonEl.classList.add("memochron-color-button"); + } + + private showColorPicker(source: CalendarSource, index: number, button: ButtonComponent) { + const modal = new ColorPickerModal(this.app, source.color || CALENDAR_COLOR_PALETTE[0], async (selectedColor) => { + this.plugin.settings.calendarUrls[index].color = selectedColor; + await this.plugin.saveSettings(); + // Force refresh to update event colors + await this.plugin.refreshCalendarView(true); + this.updateColorButton(button.buttonEl, selectedColor); + }); + modal.open(); + } + private renderFirstDayOfWeek(): void { const weekdays = [ { value: "0", label: "Sunday" }, @@ -228,6 +297,32 @@ export class SettingsTab extends PluginSettingTab { ); } + private renderEnableCalendarColors(): void { + new Setting(this.containerEl) + .setName("Enable calendar colors") + .setDesc("Show calendars in different colors for easy identification") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableCalendarColors) + .onChange(async (value) => { + this.plugin.settings.enableCalendarColors = value; + + // Auto-assign colors to existing calendars if enabling + if (value) { + this.plugin.settings.calendarUrls.forEach((source, index) => { + if (!source.color) { + source.color = CALENDAR_COLOR_PALETTE[index % CALENDAR_COLOR_PALETTE.length]; + } + }); + } + + await this.plugin.saveSettings(); + await this.plugin.refreshCalendarView(true); // Force refresh to update colors + this.display(); // Refresh settings display to show/hide color pickers + }) + ); + } + private renderRefreshInterval(): void { new Setting(this.containerEl) .setName("Refresh interval") @@ -578,4 +673,55 @@ class FilePickerModal extends SuggestModal { onChooseSuggestion(file: TFile) { this.onChoose(file); } +} + +// Color picker modal for calendar colors +class ColorPickerModal extends Modal { + constructor( + app: App, + private currentColor: string, + private onChoose: (color: string) => void + ) { + super(app); + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + + contentEl.createEl("h3", { text: "Choose Calendar Color" }); + + const colorGrid = contentEl.createEl("div", { cls: "memochron-color-grid" }); + + const colorNames = [ + "Red", "Blue", "Green", "Purple", + "Orange", "Yellow", "Pink", "Cyan" + ]; + + CALENDAR_COLOR_PALETTE.forEach((color, index) => { + const colorButton = colorGrid.createEl("button", { + cls: "memochron-color-option", + attr: { title: colorNames[index] } + }); + + colorButton.style.setProperty("--color", color); + + if (color === this.currentColor) { + colorButton.classList.add("selected"); + } + + colorButton.addEventListener("click", () => { + this.onChoose(color); + this.close(); + }); + + // Add color name label + colorButton.createEl("span", { text: colorNames[index] }); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } } \ No newline at end of file diff --git a/src/settings/types.ts b/src/settings/types.ts index 7d2c274..a405928 100644 --- a/src/settings/types.ts +++ b/src/settings/types.ts @@ -16,6 +16,7 @@ export interface CalendarSource { name: string; enabled: boolean; tags: string[]; + color?: string; // Optional color for this calendar } export interface MemoChronSettings { @@ -30,6 +31,7 @@ export interface MemoChronSettings { firstDayOfWeek: number; // 0 = Sunday, 1 = Monday, etc. hideCalendar: boolean; folderPathTemplate: string; // Template for organizing notes in date-based subfolders + enableCalendarColors: boolean; // Global toggle for calendar colors feature } export const DEFAULT_SETTINGS: MemoChronSettings = { @@ -56,4 +58,5 @@ export const DEFAULT_SETTINGS: MemoChronSettings = { firstDayOfWeek: DEFAULT_FIRST_DAY_OF_WEEK, hideCalendar: false, folderPathTemplate: "", // Empty by default for backwards compatibility + enableCalendarColors: false, // Disabled by default }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 100f659..bc66649 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -20,4 +20,17 @@ export const TEMPLATE_VARIABLES = { DATE: '{{date}}', DESCRIPTION: '{{description}}', LOCATION: '{{location}}', -}; \ No newline at end of file +}; + +// Color palette for auto-assigning calendar colors +// Using CSS variables that work well with Obsidian themes +export const CALENDAR_COLOR_PALETTE = [ + 'var(--color-red)', + 'var(--color-blue)', + 'var(--color-green)', + 'var(--color-purple)', + 'var(--color-orange)', + 'var(--color-yellow)', + 'var(--color-pink)', + 'var(--color-cyan)', +]; \ No newline at end of file diff --git a/src/views/CalendarView.ts b/src/views/CalendarView.ts index 72a2858..3aa91f3 100644 --- a/src/views/CalendarView.ts +++ b/src/views/CalendarView.ts @@ -57,6 +57,9 @@ export class CalendarView extends ItemView { ); this.renderMonth(); + // Update calendar visibility based on current settings + this.updateCalendarVisibility(); + // Always show agenda for selected date or today const dateToShow = this.selectedDate || new Date(); this.showDayAgenda(dateToShow); @@ -285,10 +288,38 @@ export class CalendarView extends ItemView { if (events.length > 0) { dayEl.addClass("has-events"); - dayEl.createEl("div", { - cls: "memochron-event-dot", - text: "•", - }); + + if (this.plugin.settings.enableCalendarColors) { + // Group events by calendar source to show one dot per calendar + const eventsBySource = new Map(); + events.forEach(event => { + if (!eventsBySource.has(event.sourceId)) { + eventsBySource.set(event.sourceId, event); + } + }); + + // Create a container for dots + const dotsContainer = dayEl.createEl("div", { + cls: "memochron-event-dots-container" + }); + + // Add a colored dot for each calendar that has events + eventsBySource.forEach(event => { + const dot = dotsContainer.createEl("div", { + cls: "memochron-event-dot colored", + text: "•", + }); + if (event.color) { + dot.style.color = event.color; + } + }); + } else { + // Single dot for all events when colors are disabled + dayEl.createEl("div", { + cls: "memochron-event-dot", + text: "•", + }); + } } } @@ -342,6 +373,12 @@ export class CalendarView extends ItemView { eventEl.addClass("past-event"); } + // Add colored left border if colors are enabled + if (this.plugin.settings.enableCalendarColors && event.color) { + eventEl.addClass("with-color"); + eventEl.style.setProperty("--event-color", event.color); + } + this.renderEventTime(eventEl, event); this.renderEventTitle(eventEl, event); this.renderEventLocation(eventEl, event); diff --git a/styles.css b/styles.css index 267d48e..974620d 100644 --- a/styles.css +++ b/styles.css @@ -153,6 +153,21 @@ margin-bottom: 4px; } +/* Colored dots container */ +.memochron-event-dots-container { + display: flex; + gap: 2px; + justify-content: center; + flex-wrap: wrap; + margin-top: 2px; +} + +.memochron-event-dot.colored { + font-size: 1em; + height: auto; + margin-bottom: 0; +} + /* Week View */ .memochron-week-grid { display: grid; @@ -274,18 +289,108 @@ border-radius: var(--radius-s); cursor: pointer; font-size: 0.9em; + position: relative; + overflow: hidden; } .memochron-agenda-event:hover { background: var(--background-modifier-hover); } +/* Colored left border for events */ +.memochron-agenda-event.with-color { + padding-left: calc(var(--memochron-padding-sm) + 4px); +} + +.memochron-agenda-event.with-color::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background-color: var(--event-color); + border-radius: var(--radius-s) 0 0 var(--radius-s); +} + /* Grey out past events in agenda view */ .memochron-agenda-event.past-event { opacity: 0.8; color: var(--text-muted); } +/* Color picker styles */ +.memochron-color-button { + position: relative; +} + +.memochron-color-button::after { + content: ""; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 12px; + border-radius: 50%; + background-color: var(--selected-color); + border: 1px solid var(--background-modifier-border); +} + +.memochron-color-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--size-4-2); + margin: var(--size-4-4) 0; +} + +.memochron-color-option { + display: flex; + align-items: center; + justify-content: center; + padding: var(--size-4-3); + border: 2px solid var(--background-modifier-border); + border-radius: var(--radius-m); + background: var(--background-primary); + cursor: pointer; + transition: all 0.2s ease; + min-height: 50px; + position: relative; +} + +.memochron-color-option::before { + content: ""; + position: absolute; + left: var(--size-4-2); + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + border-radius: 50%; + background-color: var(--color); + border: 1px solid var(--background-modifier-border); +} + +.memochron-color-option span { + margin-left: var(--size-4-6); + font-size: 0.9em; + color: var(--text-normal); +} + +.memochron-color-option:hover { + border-color: var(--interactive-accent); + background: var(--background-modifier-hover); +} + +.memochron-color-option.selected { + border-color: var(--interactive-accent); + background: var(--interactive-accent); +} + +.memochron-color-option.selected span { + color: var(--text-on-accent); +} + .memochron-event-time { color: var(--text-muted); font-size: 0.85em; @@ -323,6 +428,75 @@ border-bottom: none; } +/* Improve calendar source settings layout */ +.memochron-calendar-list .setting-item { + min-height: auto; +} + +.memochron-calendar-list .setting-item .setting-item-control { + display: flex; + flex-wrap: wrap; + gap: var(--size-4-2); + align-items: flex-start; + min-height: 2.5rem; +} + +.memochron-calendar-list .setting-item .setting-item-control > * { + margin: 0 !important; +} + +/* URL input should be wider */ +.memochron-calendar-list .setting-item input[type="text"]:first-of-type { + min-width: 200px; + flex: 2; +} + +/* Name input */ +.memochron-calendar-list .setting-item input[type="text"]:nth-of-type(2) { + min-width: 100px; + flex: 1; +} + +/* Tags input */ +.memochron-calendar-list .setting-item input[type="text"]:nth-of-type(3) { + min-width: 80px; + flex: 1; +} + +/* Color button styling */ +.memochron-color-button { + min-width: 70px; + flex-shrink: 0; +} + +/* File picker button */ +.memochron-calendar-list .setting-item button[aria-label*="folder"] { + flex-shrink: 0; +} + +/* Toggle and remove buttons */ +.memochron-calendar-list .setting-item .checkbox-container, +.memochron-calendar-list .setting-item button:last-child { + flex-shrink: 0; +} + +/* Force break to new line when needed */ +@media screen and (max-width: 1200px) { + .memochron-calendar-list .setting-item .setting-item-control { + flex-direction: column; + align-items: stretch; + } + + .memochron-calendar-list .setting-item .setting-item-control > * { + width: 100%; + margin-bottom: var(--size-4-1) !important; + } + + .memochron-calendar-list .setting-item input[type="text"] { + min-width: auto; + } +} + .memochron-setting-item-container { position: relative; }