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

+ @_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"