Skip to content

Commit b1a77f8

Browse files
v.1.1.3 cleanup
v.1.1.3 cleanup
2 parents c1e29c3 + a0fb0e0 commit b1a77f8

21 files changed

+551
-146
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@typescript-eslint/no-unused-vars": ["off", { "args": "none" }],
1919
"@typescript-eslint/ban-ts-comment": "off",
2020
"no-prototype-builtins": "off",
21-
"@typescript-eslint/no-empty-function": "off"
21+
"@typescript-eslint/no-empty-function": "off",
22+
"@typescript-eslint/no-explicit-any": "off"
2223
}
2324
}

README.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# iCalObsidianSync
2-
This Obsidian plugin allows to synchronize iCalendar with your Obsidian notes.
2+
This Obsidian plugin allows to synchronize iCalendar (and soon Google Calendar too!) with your Obsidian notes.
33

44
<p align="center">
55
<img width="650" src="https://raw.githubusercontent.com/Vaccarini-Lorenzo/iCalObsidianSync/main/materials/iCalDemo.gif">
@@ -24,24 +24,36 @@ That's it. Just write an event and the plugin will try its best to identify it.
2424

2525
# How it works
2626
### NLP module
27-
The plugin works on top of a **NLP** library [(NLP wink)](https://winkjs.org/wink-nlp/). The NLP module associates dates to intentions, nouns and proper names. At the moment the project is still in an early stage so some event might not be recognized or may not be accurate.
27+
The plugin works on top of a **NLP** library [(NLP wink)](https://winkjs.org/wink-nlp/). <br>
28+
First, the sentence is split into tokens, entities and Part-of-Speeches. Once the sentence has been broken down into understandable components, it's time to filter them following common patterns that include dates, times, durations, event-related nouns and purposes. In order to keep iCalSync lightweight, the number of patterns is not huge, nevertheless the recognition process scores high levels of precision.
2829

2930
### iCloud module
3031
The communication with iCloud wouldn't be possible without the help of [iCloud.js](https://github.com/foxt/icloud.js.git). The library has been opportunely modified to support POST requests and bypass CORS policies. <br>
3132
Since Apple doesn't support OAuth, it's necessary to login with email and password. These inserted credentials are stored exclusively in your local device (AES encrypted) in order to avoid a manual login everytime a token refresh is needed. The encryption key is randomly generated when the plugin is installed. It can be manually changed in the settings section (not recommended).
3233

3334

3435
# What's new?
36+
### v.1.1.3
37+
- Inline event view beta
38+
- Bugfix: non-editable widget bug
39+
- NPL module improvements:
40+
1) Fine-tuning
41+
### v.1.1.2
42+
- Implement internal counter to keep track of the number of cumulative synchronizations
43+
- Community review adjustments
44+
### v.1.1.1
45+
- Bugfix: date parsing
3546
### v.1.1.0
3647
- No need for a CORS proxy anymore
3748
- NPL module improvements:
38-
1) Entity-related attributes identification
39-
2) Event purpose recognition
40-
3) Fix entity overlap bug
49+
1) Entity-related attributes identification
50+
2) Event purpose recognition
51+
3) Bugfix: entity overlap
4152

4253
# Currently in development phase
43-
- **Event inline view**: From version v.1.2.0 will be possible to embed iCalendar events in your notes
44-
54+
- **Event inline view**: From version v.1.2.0 will be possible to embed iCalendar events in your notes. The user will be able to interact with the events directly and automatically synchronize the event with their calendar
4555
<p align="center">
4656
<img width="650" src="https://raw.githubusercontent.com/Vaccarini-Lorenzo/iCalObsidianSync/main/materials/inlineViewDemo.gif">
4757
</p>
58+
59+
- **Google calendar integration**: Project refactor to support multiple calendars

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "ical-obsidian-sync",
33
"name": "iCalSync",
4-
"version": "1.1.2",
4+
"version": "1.1.3",
55
"minAppVersion": "0.15.0",
66
"description": "Synchronize iCalendar with your notes",
77
"author": "Vaccarini Lorenzo",

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{
22
"name": "obsidian-sample-plugin",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
55
"main": "main.js",
66
"scripts": {
77
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs",
8-
"version": "node version-bump.mjs && git add manifest.json versions.json"
8+
"version": "node version-bump.mjs && git add manifest.json versions.json",
9+
"pack": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs && cp main.js ical-obsidian-sync && cp manifest.json ical-obsidian-sync && cp styles.css ical-obsidian-sync && cp .noun_patterns.txt ical-obsidian-sync && cp .proper_name_patterns.txt ical-obsidian-sync",
10+
"dev": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production && cp main.js /Users/lorenzovaccarini/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/Obsidian\\ Vault/.obsidian/plugins/ical-obsidian-sync && cp .noun_patterns.txt /Users/lorenzovaccarini/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/Obsidian\\ Vault/.obsidian/plugins/ical-obsidian-sync && cp .proper_name_patterns.txt /Users/lorenzovaccarini/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/Obsidian\\ Vault/.obsidian/plugins/ical-obsidian-sync && cp styles.css /Users/lorenzovaccarini/Library/Mobile\\ Documents/iCloud~md~obsidian/Documents/Obsidian\\ Vault/.obsidian/plugins/ical-obsidian-sync"
911
},
1012
"keywords": [],
1113
"author": "",

src/controllers/cacheController.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {CacheCheck} from "../model/cacheCheck";
2+
import {DateRange} from "../model/dateRange";
3+
4+
class CacheController {
5+
// TODO: Implement
6+
checkCache(dateRange: DateRange): CacheCheck {
7+
return new CacheCheck([dateRange], []);
8+
}
9+
}
10+
11+
const cacheController = new CacheController();
12+
export default cacheController;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import eventController from "./eventController";
2+
import {DateRange} from "../model/dateRange";
3+
import {iCloudCalendarEvent} from "../iCloudJs/calendar";
4+
import {CalendarViewDetail} from "../model/calendarViewDetail";
5+
import {CalendarView} from "../plugin/calendarView";
6+
import {Misc} from "../misc/misc";
7+
8+
class CalendarViewController {
9+
async getMarkdownPostProcessor(element, context){
10+
const codeblocks = element.querySelectorAll("code");
11+
const codeComponents = calendarViewController.checkCodeBlocks(codeblocks);
12+
if (codeComponents == null) return;
13+
const eventList = await calendarViewController.getEventList(codeComponents);
14+
const calendarViewData = calendarViewController.getCalendarViewData( new DateRange(new Date(codeComponents.from), new Date(codeComponents.to)), eventList);
15+
context.addChild(new CalendarView(codeComponents.codeBlock, calendarViewData));
16+
}
17+
18+
checkCodeBlocks(codeBlocks): {codeBlock, from, to} | null {
19+
if (codeBlocks.length == 0) return null;
20+
const codeBlock = codeBlocks.item(0);
21+
const codeText = codeBlock.innerText.replaceAll(" ", "");
22+
const isCal = codeText.substring(0, 6) == "<ical>";
23+
if (!isCal) return null;
24+
let from = calendarViewController.matchRegex("from:", codeText);
25+
if(from == undefined) return null;
26+
from = from.replaceAll("from:", "");
27+
let to = calendarViewController.matchRegex("to:", codeText);
28+
if(to == undefined) to = from;
29+
else to = to.replaceAll("to:", "");
30+
return {codeBlock, from, to};
31+
}
32+
33+
private matchRegex(prefix, text): string | undefined{
34+
// Constructing the regular expression pattern
35+
const pattern = `${prefix}\\d{4}(\\/|-)\\d{1,2}(\\/|-)\\d{1,2}`;
36+
const matches = text.replaceAll(" ", "").match(pattern);
37+
if (matches == null) return undefined;
38+
return matches.filter(match => match.length > 4).first();
39+
}
40+
41+
private async getEventList(codeComponents: { from; to }): Promise<iCloudCalendarEvent[] | []> {
42+
const dateRange = new DateRange(new Date(codeComponents.from), new Date(codeComponents.to));
43+
return await eventController.getEventsFromRange(dateRange);
44+
}
45+
46+
private getCalendarViewData(dateRange: DateRange, eventList: iCloudCalendarEvent[] | []): {numOfCols, numOfRows, rowNeedsLabelMap, calendarViewDetails, startDate} {
47+
const calendarViewDetails: CalendarViewDetail[] = [];
48+
const noOverlapMap = this.manageEventOverlap(eventList, dateRange);
49+
const auxStruct = this.getAuxiliaryStructure(dateRange);
50+
const rowNeedsLabelMap = new Map<number, boolean>();
51+
const filledRows = this.fillCalendarViewDetails(noOverlapMap, rowNeedsLabelMap, auxStruct, calendarViewDetails);
52+
53+
return {
54+
numOfCols: auxStruct.numOfCols,
55+
numOfRows: filledRows,
56+
rowNeedsLabelMap,
57+
calendarViewDetails,
58+
startDate: dateRange.start
59+
}
60+
}
61+
62+
private getAuxiliaryStructure(dateRange: DateRange) : {numOfCols, refiner, refinerMinutes, minTimeMilli, milliInDay} {
63+
const numOfConsideredHours = 24;
64+
// Every 15 mins
65+
const refiner = 2;
66+
const refinerMinutes = 60 / refiner;
67+
const numOfCols = numOfConsideredHours * refiner;
68+
69+
const minTimeMilli = dateRange.start.getTime();
70+
const milliInDay = 1000 * 3600 * 24;
71+
72+
return {
73+
numOfCols,
74+
refiner,
75+
refinerMinutes,
76+
minTimeMilli,
77+
milliInDay
78+
}
79+
}
80+
81+
private manageEventOverlap(eventList: iCloudCalendarEvent[] | [], dateRange: DateRange) {
82+
const date = new Date(dateRange.start);
83+
const dayDiff = dateRange.getDayDifference();
84+
85+
const noOverlapMap = new Map<Date, iCloudCalendarEvent[][]>();
86+
for (let i = 0; i <= dayDiff; i++){
87+
const dayEvents = this.filterEventsWithStartDate(eventList, date);
88+
this.checkOverlaps(dayEvents, noOverlapMap, date);
89+
date.setDate(date.getDate() + 1);
90+
}
91+
return noOverlapMap;
92+
}
93+
94+
private filterEventsWithStartDate(eventList: iCloudCalendarEvent[], date: Date): iCloudCalendarEvent[] {
95+
return eventList.filter(event => {
96+
const eventDate = Misc.getDateFromICloudArray(event.startDate);
97+
return eventDate.toLocaleDateString() == date.toLocaleDateString();
98+
})
99+
}
100+
101+
private checkOverlaps(dayEvents: iCloudCalendarEvent[], noOverlapMap, date: Date){
102+
const sortedDayEvents = Misc.sortICloudCalendarEventList(dayEvents);
103+
const toCheckList = [...sortedDayEvents];
104+
const eventRows = [];
105+
106+
sortedDayEvents.forEach((dayEvent, dayEventsIndex) => {
107+
if (!toCheckList.contains(dayEvent)) return;
108+
toCheckList.remove(dayEvent);
109+
const noOverlapList: iCloudCalendarEvent[] = [dayEvent];
110+
const dateRange = new DateRange(Misc.getDateFromICloudArray(dayEvent.startDate), Misc.getDateFromICloudArray(dayEvent.endDate));
111+
for (let i = dayEventsIndex + 1; i < sortedDayEvents.length; i++){
112+
const nextEvent = sortedDayEvents[i];
113+
const nextDateRange = new DateRange(Misc.getDateFromICloudArray(nextEvent.startDate), Misc.getDateFromICloudArray(nextEvent.endDate));
114+
if (dateRange.overlaps(nextDateRange)) continue;
115+
toCheckList.remove(nextEvent);
116+
noOverlapList.push(nextEvent);
117+
}
118+
this.propagateListOverlapCheck(noOverlapList, toCheckList);
119+
eventRows.push(noOverlapList);
120+
121+
})
122+
noOverlapMap.set(new Date(date), eventRows);
123+
}
124+
125+
private propagateListOverlapCheck(noOverlapList: iCloudCalendarEvent[], toCheckList: iCloudCalendarEvent[]) {
126+
127+
noOverlapList.forEach((noOverlapEvent, noOverlapIndex) => {
128+
const noOverlapEventDateRange = new DateRange(Misc.getDateFromICloudArray(noOverlapEvent.startDate), Misc.getDateFromICloudArray(noOverlapEvent.endDate));
129+
for (let i = noOverlapIndex + 1; i < noOverlapList.length; i++){
130+
const check = noOverlapList[i];
131+
const checkDateRange = new DateRange(Misc.getDateFromICloudArray(check.startDate), Misc.getDateFromICloudArray(check.endDate));
132+
if (noOverlapEventDateRange.overlaps(checkDateRange)){
133+
noOverlapList.remove(check);
134+
toCheckList.push(check);
135+
}
136+
}
137+
})
138+
}
139+
140+
private fillCalendarViewDetails(noOverlapMap, rowNeedsLabelMap, auxStruct, calendarViewDetails): number {
141+
let rowIndex = 0;
142+
143+
Array.from(noOverlapMap.entries()).forEach((noOverlapEntry) => {
144+
const eventBlocks = noOverlapEntry[1];
145+
if (eventBlocks.length == 0){
146+
rowNeedsLabelMap.set(rowIndex, true);
147+
rowIndex += 1;
148+
return;
149+
}
150+
let isOverlap = false;
151+
eventBlocks.forEach(noOverlapArray => {
152+
noOverlapArray.forEach(noOverlapEvent => {
153+
if (isOverlap){
154+
rowNeedsLabelMap.set(rowIndex, false);
155+
} else {
156+
rowNeedsLabelMap.set(rowIndex, true);
157+
}
158+
const eventStartTime = Misc.getDateFromICloudArray(noOverlapEvent.startDate)
159+
const eventEndTime = Misc.getDateFromICloudArray(noOverlapEvent.endDate)
160+
const fromCol = eventStartTime.getHours() * auxStruct.refiner + eventStartTime.getMinutes() / auxStruct.refinerMinutes
161+
const toCol = eventEndTime.getHours() * auxStruct.refiner + eventEndTime.getMinutes() / auxStruct.refinerMinutes
162+
const row = rowIndex;
163+
const title = noOverlapEvent.title;
164+
const calendarViewDetail = new CalendarViewDetail(title, row, fromCol, toCol)
165+
calendarViewDetails.push(calendarViewDetail);
166+
})
167+
isOverlap = true;
168+
rowIndex += 1;
169+
})
170+
})
171+
172+
return rowIndex;
173+
}
174+
}
175+
176+
const calendarViewController = new CalendarViewController();
177+
export default calendarViewController;

src/controllers/eventController.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import iCloudController from "./iCloudController";
66
import {Notice, requestUrl, RequestUrlParam} from "obsidian";
77
import {appendFileSync, readFileSync, writeFileSync} from "fs";
88
import {DateRange} from "../model/dateRange";
9+
import cacheController from "./cacheController";
910

1011
class EventController{
1112
// Map that connects the file path to the list of events
@@ -174,6 +175,18 @@ class EventController{
174175
console.warn("Error interacting with the counter server", e);
175176
}
176177
}
178+
179+
async getEventsFromRange(dateRange: DateRange): Promise<iCloudCalendarEvent[]> {
180+
const cacheCheck = cacheController.checkCache(dateRange);
181+
if (cacheCheck.missedDateRanges.length == 0) return cacheCheck.cachedICouldEvents;
182+
const iCloudEvents = cacheCheck.cachedICouldEvents;
183+
for (let i=0; i<cacheCheck.missedDateRanges.length; i++){
184+
const missedDateRange = cacheCheck.missedDateRanges[i];
185+
const fetchedICloudEvents = await iCloudController.getICloudEvents(missedDateRange);
186+
fetchedICloudEvents.forEach(iCloudEvent => iCloudEvents.push(iCloudEvent));
187+
}
188+
return iCloudEvents;
189+
}
177190
}
178191

179192
const eventController = new EventController();

src/controllers/iCloudController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class ICloudController {
8989
}
9090

9191
async getICloudEvents(missedDateRange: DateRange): Promise<iCloudCalendarEvent[]> {
92+
if (this._calendarService == undefined) return [];
9293
return await this._calendarService.events(missedDateRange.start, missedDateRange.end);
9394
}
9495

0 commit comments

Comments
 (0)