mirror of
https://github.com/formax68/memoChron.git
synced 2026-07-22 05:45:44 +00:00
add URL validation guardrail for incorrect calendar URLs
- detect incorrect Google Calendar URL formats (public link, embed, app link) - show helpful error message with "How do I get the correct URL?" button - add help modal with instructions for Google, Outlook, and iCloud calendars - update README with detailed Google Calendar setup instructions - fixes #57
This commit is contained in:
parent
cffedd314f
commit
99d26a9508
4 changed files with 309 additions and 5 deletions
23
README.md
23
README.md
|
|
@ -45,10 +45,31 @@ MemoChron supports both remote calendar URLs and local ICS files.
|
|||
|
||||
1. Go to Settings > MemoChron
|
||||
2. Click "Add Calendar"
|
||||
3. Enter your public iCalendar (ICS) URL (e.g., `https://calendar.google.com/calendar/ical/...`)
|
||||
3. Enter your public iCalendar (ICS) URL
|
||||
4. Give your calendar a name and optional tags
|
||||
5. Enable/disable calendars as needed
|
||||
|
||||
##### Getting the Correct URL from Google Calendar
|
||||
|
||||
**Important**: You need the iCal feed URL, not the public calendar link. The public link opens a webpage but doesn't provide the calendar data that MemoChron needs.
|
||||
|
||||
To get the correct URL from Google Calendar:
|
||||
|
||||
1. Open [Google Calendar](https://calendar.google.com) in your browser
|
||||
2. Click the gear icon (⚙️) → **Settings**
|
||||
3. In the left sidebar, click on the calendar you want to add
|
||||
4. Scroll down to **Integrate calendar**
|
||||
5. Copy the **Secret address in iCal format** (recommended) or **Public address in iCal format**
|
||||
|
||||

|
||||
|
||||
**Correct URL format**: `https://calendar.google.com/calendar/ical/.../basic.ics`
|
||||
|
||||
**Incorrect URL formats** (these won't work):
|
||||
- `https://calendar.google.com/calendar/u/0?cid=...` (public link - opens a webpage)
|
||||
- `https://calendar.google.com/calendar/embed?src=...` (embed link - for websites)
|
||||
- `https://calendar.google.com/calendar/r/...` (calendar app link)
|
||||
|
||||
#### Local ICS Files
|
||||
|
||||
You can also use ICS files stored in your Obsidian vault or local file system:
|
||||
|
|
|
|||
BIN
screenshots/gcal-ical-url.png
Normal file
BIN
screenshots/gcal-ical-url.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
|
|
@ -296,11 +296,27 @@ export class SettingsTab extends PluginSettingTab {
|
|||
if (!validation.valid) {
|
||||
errorEl = urlSetting.descEl.createDiv({
|
||||
cls: "memochron-error-message",
|
||||
text: validation.error,
|
||||
});
|
||||
errorEl.style.color = "var(--text-error, #c92424)";
|
||||
errorEl.style.fontSize = "0.9em";
|
||||
errorEl.style.marginTop = "0.5em";
|
||||
|
||||
errorEl.createSpan({ text: validation.error });
|
||||
|
||||
// If it's a wrong URL type, show a help button
|
||||
if (validation.isWrongUrlType) {
|
||||
errorEl.createEl("br");
|
||||
const helpBtn = errorEl.createEl("button", {
|
||||
text: "How do I get the correct URL?",
|
||||
cls: "memochron-help-btn",
|
||||
});
|
||||
helpBtn.style.marginTop = "0.5em";
|
||||
helpBtn.style.fontSize = "0.85em";
|
||||
this.plugin.registerDomEvent(helpBtn, "click", (e) => {
|
||||
e.preventDefault();
|
||||
new CalendarUrlHelpModal(this.app, this.plugin).open();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Save valid URL
|
||||
this.plugin.settings.calendarUrls[index].url = value;
|
||||
|
|
@ -1206,17 +1222,75 @@ export class SettingsTab extends PluginSettingTab {
|
|||
.filter((tag) => tag.length > 0);
|
||||
}
|
||||
|
||||
private validateCalendarUrl(url: string): { valid: boolean; error?: string } {
|
||||
private validateCalendarUrl(url: string): { valid: boolean; error?: string; warning?: string; isWrongUrlType?: boolean } {
|
||||
if (!url || url.trim() === "") {
|
||||
return { valid: false, error: "URL or file path is required" };
|
||||
}
|
||||
|
||||
const trimmedUrl = url.trim();
|
||||
const trimmedUrl = url.trim().toLowerCase();
|
||||
|
||||
// Check for HTTP/HTTPS URLs
|
||||
if (trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://")) {
|
||||
try {
|
||||
new URL(trimmedUrl);
|
||||
const parsedUrl = new URL(url.trim());
|
||||
|
||||
// Detect incorrect Google Calendar URL formats
|
||||
if (parsedUrl.hostname.includes("calendar.google.com")) {
|
||||
// Check for common wrong URL patterns
|
||||
|
||||
// Public link format: calendar.google.com/calendar/u/0?cid=...
|
||||
if (parsedUrl.searchParams.has("cid") ||
|
||||
parsedUrl.pathname.match(/\/calendar\/u\/\d+$/)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "This is a Google Calendar public link, not an iCal feed URL.",
|
||||
isWrongUrlType: true
|
||||
};
|
||||
}
|
||||
|
||||
// Embed format: calendar.google.com/calendar/embed?src=...
|
||||
if (parsedUrl.pathname.includes("/embed")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "This is a Google Calendar embed link, not an iCal feed URL.",
|
||||
isWrongUrlType: true
|
||||
};
|
||||
}
|
||||
|
||||
// Calendar app link: calendar.google.com/calendar/r/...
|
||||
if (parsedUrl.pathname.includes("/calendar/r")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "This is a Google Calendar app link, not an iCal feed URL.",
|
||||
isWrongUrlType: true
|
||||
};
|
||||
}
|
||||
|
||||
// Valid Google Calendar ICS URL should contain /ical/ and end with .ics
|
||||
if (!parsedUrl.pathname.includes("/ical/") || !parsedUrl.pathname.endsWith(".ics")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "This doesn't appear to be a valid Google Calendar iCal URL.",
|
||||
isWrongUrlType: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Detect incorrect Outlook/Office365 URL formats
|
||||
if (parsedUrl.hostname.includes("outlook.live.com") ||
|
||||
parsedUrl.hostname.includes("outlook.office365.com") ||
|
||||
parsedUrl.hostname.includes("outlook.office.com")) {
|
||||
// Check if it's a calendar sharing link, not an ICS feed
|
||||
if (parsedUrl.pathname.includes("/calendar/published/") &&
|
||||
!parsedUrl.pathname.endsWith(".ics")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "This appears to be an Outlook sharing link. Please use the ICS subscription URL instead.",
|
||||
isWrongUrlType: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch {
|
||||
return { valid: false, error: "Invalid URL format" };
|
||||
|
|
@ -1713,3 +1787,92 @@ class CalendarNotesSettingsModal extends Modal {
|
|||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// Help modal for calendar URL setup
|
||||
class CalendarUrlHelpModal extends Modal {
|
||||
constructor(app: App, private plugin: MemoChron) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("memochron-help-modal");
|
||||
|
||||
contentEl.createEl("h2", { text: "How to Get the Correct Calendar URL" });
|
||||
|
||||
// Google Calendar section
|
||||
const gcalSection = contentEl.createDiv({ cls: "memochron-help-section" });
|
||||
gcalSection.createEl("h3", { text: "Google Calendar" });
|
||||
|
||||
const gcalSteps = gcalSection.createEl("ol");
|
||||
gcalSteps.createEl("li", { text: "Open Google Calendar in your browser" });
|
||||
gcalSteps.createEl("li", { text: "Click the gear icon (⚙️) → Settings" });
|
||||
gcalSteps.createEl("li", { text: "In the left sidebar, click on the calendar you want to add" });
|
||||
gcalSteps.createEl("li", { text: "Scroll down to \"Integrate calendar\"" });
|
||||
gcalSteps.createEl("li").innerHTML = "Copy the <strong>Secret address in iCal format</strong>";
|
||||
|
||||
const gcalNote = gcalSection.createDiv({ cls: "memochron-help-note" });
|
||||
gcalNote.createEl("strong", { text: "Correct URL looks like: " });
|
||||
gcalNote.createEl("code", { text: "https://calendar.google.com/calendar/ical/.../basic.ics" });
|
||||
|
||||
gcalSection.createEl("hr");
|
||||
|
||||
// Outlook section
|
||||
const outlookSection = contentEl.createDiv({ cls: "memochron-help-section" });
|
||||
outlookSection.createEl("h3", { text: "Outlook / Microsoft 365" });
|
||||
|
||||
const outlookSteps = outlookSection.createEl("ol");
|
||||
outlookSteps.createEl("li", { text: "Open Outlook calendar on the web (outlook.office.com)" });
|
||||
outlookSteps.createEl("li", { text: "Click the gear icon → View all Outlook settings" });
|
||||
outlookSteps.createEl("li", { text: "Go to Calendar → Shared calendars" });
|
||||
outlookSteps.createEl("li", { text: "Under \"Publish a calendar\", select your calendar and permissions" });
|
||||
outlookSteps.createEl("li").innerHTML = "Copy the <strong>ICS link</strong> (not the HTML link)";
|
||||
|
||||
outlookSection.createEl("hr");
|
||||
|
||||
// iCloud section
|
||||
const icloudSection = contentEl.createDiv({ cls: "memochron-help-section" });
|
||||
icloudSection.createEl("h3", { text: "Apple iCloud Calendar" });
|
||||
|
||||
const icloudSteps = icloudSection.createEl("ol");
|
||||
icloudSteps.createEl("li", { text: "Open the Calendar app on your Mac" });
|
||||
icloudSteps.createEl("li", { text: "Right-click on the calendar → Share Calendar" });
|
||||
icloudSteps.createEl("li", { text: "Check \"Public Calendar\" to make it shareable" });
|
||||
icloudSteps.createEl("li", { text: "Click \"Copy Link\" to get the subscription URL" });
|
||||
|
||||
contentEl.createEl("hr");
|
||||
|
||||
// Common mistakes
|
||||
const mistakesSection = contentEl.createDiv({ cls: "memochron-help-section" });
|
||||
mistakesSection.createEl("h3", { text: "Common Mistakes" });
|
||||
|
||||
const mistakesList = mistakesSection.createEl("ul");
|
||||
mistakesList.createEl("li").innerHTML = "<strong>Using the public link</strong> - This opens a webpage, not calendar data";
|
||||
mistakesList.createEl("li").innerHTML = "<strong>Using the embed link</strong> - This is for embedding in websites";
|
||||
mistakesList.createEl("li").innerHTML = "<strong>Missing the .ics extension</strong> - The URL should end with .ics";
|
||||
|
||||
// Documentation link
|
||||
const docLink = contentEl.createDiv({ cls: "memochron-help-doc-link" });
|
||||
docLink.style.marginTop = "1em";
|
||||
const link = docLink.createEl("a", {
|
||||
text: "View full documentation on GitHub",
|
||||
href: "https://github.com/formax68/memoChron#remote-calendars",
|
||||
});
|
||||
link.setAttr("target", "_blank");
|
||||
|
||||
// Close button
|
||||
const buttonContainer = contentEl.createDiv({ cls: "memochron-help-buttons" });
|
||||
buttonContainer.style.marginTop = "1.5em";
|
||||
buttonContainer.style.textAlign = "right";
|
||||
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setButtonText("Close")
|
||||
.onClick(() => this.close());
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
120
styles.css
120
styles.css
|
|
@ -1135,4 +1135,124 @@
|
|||
.memochron-controls.calendar-hidden .memochron-nav {
|
||||
justify-content: flex-start;
|
||||
/* Align left when alone */
|
||||
}
|
||||
|
||||
/* Inline color picker styles */
|
||||
.memochron-inline-color-picker {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memochron-inline-color-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.memochron-inline-color-swatch:hover {
|
||||
transform: scale(1.1);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.memochron-inline-color-swatch.selected {
|
||||
border-color: var(--text-normal);
|
||||
box-shadow: 0 0 0 2px var(--background-primary), 0 0 0 4px var(--text-normal);
|
||||
}
|
||||
|
||||
.memochron-inline-color-custom-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memochron-inline-color-custom-label.selected svg circle {
|
||||
stroke: var(--text-normal);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Help button styling */
|
||||
.memochron-help-btn {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.memochron-help-btn:hover {
|
||||
background: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
/* Help modal styling */
|
||||
.memochron-help-modal {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.memochron-help-modal h2 {
|
||||
margin-bottom: var(--size-4-4);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.memochron-help-modal h3 {
|
||||
margin-top: var(--size-4-4);
|
||||
margin-bottom: var(--size-4-2);
|
||||
color: var(--text-normal);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.memochron-help-section {
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.memochron-help-section ol,
|
||||
.memochron-help-section ul {
|
||||
margin-left: var(--size-4-4);
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.memochron-help-section li {
|
||||
margin-bottom: var(--size-4-1);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.memochron-help-note {
|
||||
background: var(--background-secondary);
|
||||
padding: var(--size-4-2) var(--size-4-3);
|
||||
border-radius: var(--radius-s);
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
|
||||
.memochron-help-note code {
|
||||
background: var(--background-modifier-hover);
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 0.85em;
|
||||
display: inline-block;
|
||||
margin-top: var(--size-4-1);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.memochron-help-doc-link {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.memochron-help-doc-link a {
|
||||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.memochron-help-doc-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Error message styling */
|
||||
.memochron-error-message {
|
||||
color: var(--text-error);
|
||||
font-size: 0.9em;
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
Loading…
Reference in a new issue