colours in calendars

This commit is contained in:
Michalis Efstratiadis 2025-06-10 14:48:00 +03:00
parent 2ddddbfcdb
commit 6bd6d2f684
No known key found for this signature in database
7 changed files with 392 additions and 10 deletions

View file

@ -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,
};
}

View file

@ -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) {

View file

@ -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<void> {
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<TFile> {
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();
}
}

View file

@ -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
};

View file

@ -20,4 +20,17 @@ export const TEMPLATE_VARIABLES = {
DATE: '{{date}}',
DESCRIPTION: '{{description}}',
LOCATION: '{{location}}',
};
};
// 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)',
];

View file

@ -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<string, CalendarEvent>();
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);

View file

@ -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;
}