diff --git a/BlazorApp/BlazorApp.csproj b/BlazorApp/BlazorApp.csproj
index f175d22..b61f689 100644
--- a/BlazorApp/BlazorApp.csproj
+++ b/BlazorApp/BlazorApp.csproj
@@ -15,4 +15,10 @@
+
+
+ Event.razor
+
+
+
diff --git a/BlazorApp/Data/Calendar.cs b/BlazorApp/Data/Calendar.cs
index 8dd9a3b..3d13e5f 100644
--- a/BlazorApp/Data/Calendar.cs
+++ b/BlazorApp/Data/Calendar.cs
@@ -3,24 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net.Http;
+using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace BlazorApp.Data
{
- public class Event
- {
- public string Uid { get; set; }
- public string Summary { get; set; }
- public DateTime? DtStart { get; set; }
- public DateTime? DtEnd { get; set; }
- public string Description { get; set; }
- public TimeSpan? Duration { get; set; }
-
- public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
- }
-
public class Calendar
{
public int Id { get; set; }
@@ -127,17 +116,22 @@ namespace BlazorApp.Data
continue;
}
+ if (name == "END" && value == "VEVENT")
+ {
+ // End of the current event, add it to the list
+ events.Add(@event);
+ @event = null;
+ continue;
+ }
+
+ var parameters = left.Length == 2 ? left[1] : null;
+
+ // Add property to the RawProperties list.
+ // Used for rebuilding the full iCal for updating the event on the server.
+ @event.RawProperties.Add((name, parameters, value));
+
switch (name)
{
- case "END":
- if (value == "VEVENT")
- {
- // End of the current event, add it to the list
- events.Add(@event);
- @event = null;
- }
-
- break;
case "UID":
@event.Uid = value;
break;
@@ -162,18 +156,8 @@ namespace BlazorApp.Data
return events;
}
- /**
- * Retrieves and parses all events from the CalDav server.
- */
- public async Task> GetEvents()
+ private static List _filterSortEvents(List events)
{
- var client = new HttpClient();
- if (AuthorizationHeader != null)
- client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
-
- var body = await client.GetStringAsync(Url);
- var events = ParseICal(body);
-
var now = DateTime.Now;
events = events.FindAll(@event => @event.DtStart > now || @event.DtEnd > now);
@@ -190,6 +174,26 @@ namespace BlazorApp.Data
return events;
}
+ /**
+ * Retrieves and parses all events from the CalDav server.
+ */
+ public async Task> GetEvents()
+ {
+ var client = new HttpClient();
+ if (AuthorizationHeader != null)
+ client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
+
+ var response = await client.GetAsync(Url);
+ var eTag = response.Headers.ETag?.Tag;
+ var body = await response.Content.ReadAsStringAsync();
+
+ var events = ParseICal(body);
+ foreach (var @event in events)
+ @event.ETag = eTag;
+
+ return _filterSortEvents(events);
+ }
+
/**
* Retrieves and parses an event with UID uid
from the CalDav server.
*/
@@ -199,6 +203,7 @@ namespace BlazorApp.Data
if (AuthorizationHeader != null)
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
+ // Request the event with UID `uid`.
var request = new HttpRequestMessage(new HttpMethod("REPORT"), Url);
request.Headers.Add("Depth", "1");
@@ -225,13 +230,53 @@ namespace BlazorApp.Data
var result = await client.SendAsync(request);
var body = await result.Content.ReadAsStreamAsync();
+ // Parse the received XML
var reader = new XmlTextReader(body);
var document = new XmlDocument();
document.Load(reader);
- var node = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
- var events = ParseICal(node);
- return events.Count > 0 ? events[0] : null;
+ // Extract and parse the ICal data
+ var dataNode = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
+ var events = ParseICal(dataNode);
+ if (events.Count == 0)
+ return null;
+ var @event = events[0];
+
+ // Extract the ETag and event collection href.
+ @event.ETag = document.GetElementsByTagName("getetag")[0]?.InnerText;
+ @event.Href = document.GetElementsByTagName("href")[0]?.InnerText;
+
+ return @event;
+ }
+
+ public async Task UpdateEvent(Event @event)
+ {
+ var client = new HttpClient();
+ if (AuthorizationHeader != null)
+ client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
+
+ // Setting If-Match header to the ETag is required when updating an event.
+ if (@event.ETag != null)
+ client.DefaultRequestHeaders.IfMatch.Add(new EntityTagHeaderValue(@event.ETag));
+
+ // Build a new URL based on the calendar's URL and the event's collection href.
+ var url = new Uri(new Uri(Url), @event.Href);
+
+ var content = new StringContent(@event.ToICal(), Encoding.UTF8, "text/calendar");
+ var response = await client.PutAsync(url, content);
+
+ // Parse the newly updated event.
+ var body = await response.Content.ReadAsStringAsync();
+
+ var events = ParseICal(body);
+ if (events.Count == 0)
+ return null;
+ var newEvent = events[0];
+
+ // Extract the updated ETag
+ newEvent.ETag = response.Headers.ETag?.Tag;
+
+ return newEvent;
}
}
}
\ No newline at end of file
diff --git a/BlazorApp/Data/Event.cs b/BlazorApp/Data/Event.cs
new file mode 100644
index 0000000..4c3da9c
--- /dev/null
+++ b/BlazorApp/Data/Event.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BlazorApp.Data
+{
+ public class Event
+ {
+ public string Uid { get; set; }
+ public string Summary { get; set; }
+ public DateTime? DtStart { get; set; }
+ public DateTime? DtEnd { get; set; }
+ public string Description { get; set; }
+ public TimeSpan? Duration { get; set; }
+
+ public List<(string, string, string)> RawProperties { get; } = new();
+ public string ETag { get; set; }
+ public string Href { get; set; }
+
+ public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
+
+ private static string _escape(string s)
+ {
+ return s
+ .Replace(@"\", @"\\")
+ .Replace(",", @"\,")
+ .Replace(";", @"\;")
+ .Replace("\n", @"\n");
+ }
+
+ public string ToICal()
+ {
+ var sb = new StringBuilder();
+ sb.Append("BEGIN:VCALENDAR\r\n");
+ sb.Append("VERSION:2.0\r\n");
+ sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n");
+ sb.Append("BEGIN:VEVENT\r\n");
+
+ foreach (var (name, parameters, oldValue) in RawProperties)
+ {
+ sb.Append(name);
+ if (parameters != null)
+ sb.Append($";{parameters}");
+
+ var value = name switch
+ {
+ "SUMMARY" => _escape(Summary),
+ _ => oldValue
+ };
+
+ sb.Append($":{value}\r\n");
+ }
+
+ sb.Append("END:VEVENT\r\n");
+ sb.Append("END:VCALENDAR\r\n");
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BlazorApp/Pages/Event.razor b/BlazorApp/Pages/Event.razor
index 577a728..accf618 100644
--- a/BlazorApp/Pages/Event.razor
+++ b/BlazorApp/Pages/Event.razor
@@ -13,7 +13,21 @@
}
else
{
- @_event.Summary
+ @if (!_editTitle)
+ {
+
+ _editTitle = true">@_event.Summary
+
+ }
+ else
+ {
+
+
+ }
+
+
+ ETag: @_event.ETag
+
@if (_event.DtStart != null)
{
@@ -40,11 +54,18 @@ else
[Parameter]
public string EventUid { get; set; }
+ private Data.Calendar _calendar;
private Data.Event _event;
+ private bool _editTitle;
protected override async Task OnInitializedAsync()
{
- var calendar = await Data.CalendarService.GetCalendarById(CalendarId);
- _event = await calendar.GetEventByUid(EventUid);
+ _calendar = await Data.CalendarService.GetCalendarById(CalendarId);
+ _event = await _calendar.GetEventByUid(EventUid);
+ }
+
+ private async void UpdateEvent()
+ {
+ _event = await _calendar.UpdateEvent(_event);
}
}
\ No newline at end of file
diff --git a/BlazorApp/Pages/Event.razor.css b/BlazorApp/Pages/Event.razor.css
new file mode 100644
index 0000000..1f39632
--- /dev/null
+++ b/BlazorApp/Pages/Event.razor.css
@@ -0,0 +1,3 @@
+.event-summary > span:hover {
+ border: 1px solid rgba(0, 0, 0, 0.2);
+}
\ No newline at end of file
diff --git a/BlazorApp/Properties/launchSettings.json b/BlazorApp/Properties/launchSettings.json
index 0ac6e4e..6e4af82 100644
--- a/BlazorApp/Properties/launchSettings.json
+++ b/BlazorApp/Properties/launchSettings.json
@@ -18,7 +18,7 @@
"BlazorApp": {
"commandName": "Project",
"dotnetRunMessages": "true",
- "launchBrowser": true,
+ "launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"