From 03c9ac1e11dd90f2447291e97b1429d84a484726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Mon, 7 Jul 2025 21:03:07 +0200 Subject: [PATCH] Local time rendering for events. --- .../AccountManagementViewModel.cs | 11 +++ .../CalendarPageViewModel.cs | 9 ++ .../Data/CalendarItemViewModel.cs | 52 +++++++++++- Wino.Calendar/Controls/WinoCalendarPanel.cs | 8 +- Wino.Calendar/Views/AppShell.xaml | 1 + .../Entities/Calendar/CalendarItem.cs | 2 +- .../Interfaces/ICalendarItemViewModel.cs | 8 +- .../Synchronizers/OutlookSynchronizer.cs | 3 +- Wino.Services/CalendarServiceEx.cs | 83 ++++++++++++------- 9 files changed, 136 insertions(+), 41 deletions(-) diff --git a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs index f7eb9563..1709eb60 100644 --- a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; @@ -141,6 +142,16 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel Type = CalendarSynchronizationType.CalendarMetadata }; + var timer = new Stopwatch(); + var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client)); + + timer.Stop(); + + Debug.WriteLine("Synchronization completed in {timer.ElapsedMilliseconds} ms"); + + // TODO: Properly handle synchronization errors. + + accountCreationDialog.Complete(false); } } diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 606a951a..f2b0fcb3 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -789,6 +789,13 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, // Recurring events must be selected as a single instance. // We need to find the day that the event is in, and then select the event. + if (calendarItemViewModel == null) return Enumerable.Empty(); + + // If the calendar item is not recurring, we can just return it. + + if (calendarItemViewModel.ItemType == CalendarItemType.Timed) return new[] { calendarItemViewModel }; + + // TODO: Implement below logic. return default; @@ -825,6 +832,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay); + if (itemsToSelect == null) return; + foreach (var item in itemsToSelect) { item.IsSelected = true; diff --git a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs index 6f69aa24..430f46b8 100644 --- a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using Itenso.TimePeriod; using Wino.Core.Domain.Entities.Calendar; @@ -22,6 +23,16 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC public DateTime EndDateTime => CalendarItem.EndDateTime; + /// + /// Gets the start date and time in the local time zone for display purposes. + /// + public DateTime LocalStartDateTime => ConvertToLocalTime(); + + /// + /// Gets the end date and time in the local time zone for display purposes. + /// + public DateTime LocalEndDateTime => ConvertToLocalTime(); + public ITimePeriod Period => CalendarItem.Period; public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.RecurrenceRules) || !string.IsNullOrEmpty(CalendarItem.RecurringEventId); @@ -31,11 +42,50 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC public ObservableCollection Attendees { get; } = new ObservableCollection(); - public CalendarItemType ItemType => ((ICalendarItem)CalendarItem).ItemType; + public CalendarItemType ItemType => CalendarItem.ItemType; public CalendarItemViewModel(CalendarItem calendarItem) { CalendarItem = calendarItem; + + Debug.WriteLine($"{Title} : {ItemType}"); + } + + /// + /// Converts a DateTime to local time based on the provided timezone. + /// If timezone is empty or null, assumes the DateTime is in UTC. + /// + /// The DateTime to convert + /// The timezone string. If empty/null, assumes UTC. + /// DateTime converted to local time + private DateTime ConvertToLocalTime() + { + // All day events ignore time zones and are treated as local time. + if (ItemType == CalendarItemType.AllDay || ItemType == CalendarItemType.MultiDayAllDay || ItemType == CalendarItemType.RecurringAllDay) + return CalendarItem.StartDateTime; + + if (string.IsNullOrEmpty(CalendarItem.TimeZone)) + { + // If no timezone specified, assume it's UTC and convert to local time + return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime(); + } + + try + { + // Parse the timezone and convert to local time + var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.TimeZone); + return TimeZoneInfo.ConvertTimeToUtc(CalendarItem.StartDateTime, sourceTimeZone).ToLocalTime(); + } + catch (TimeZoneNotFoundException) + { + // If timezone is not found, fallback to treating as UTC + return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime(); + } + catch (InvalidTimeZoneException) + { + // If timezone is invalid, fallback to treating as UTC + return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime(); + } } public override string ToString() => CalendarItem.Title; diff --git a/Wino.Calendar/Controls/WinoCalendarPanel.cs b/Wino.Calendar/Controls/WinoCalendarPanel.cs index 68e99283..135ab574 100644 --- a/Wino.Calendar/Controls/WinoCalendarPanel.cs +++ b/Wino.Calendar/Controls/WinoCalendarPanel.cs @@ -45,9 +45,9 @@ public partial class WinoCalendarPanel : Panel private void ResetMeasurements() => _measurements.Clear(); - private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight) + private double GetChildTopMargin(ICalendarItemViewModel calendarItemViewModel, double availableHeight) { - var childStart = calendarItemViewModel.StartDateTime; + var childStart = calendarItemViewModel.LocalStartDateTime; if (childStart <= Period.Start) { @@ -69,7 +69,7 @@ public partial class WinoCalendarPanel : Panel private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth) => availableWidth * calendarItemMeasurement.Left; - private double GetChildHeight(ICalendarItem child) + private double GetChildHeight(ICalendarItemViewModel child) { // All day events are not measured. if (child.ItemType == CalendarItemType.AllDay) return 0; @@ -128,7 +128,7 @@ public partial class WinoCalendarPanel : Panel foreach (var control in calendarControls) { // We can't arrange this child. - if (!(control.Content is ICalendarItem child)) continue; + if (!(control.Content is ICalendarItemViewModel child)) continue; bool isHorizontallyLastItem = false; diff --git a/Wino.Calendar/Views/AppShell.xaml b/Wino.Calendar/Views/AppShell.xaml index a8b31d8f..5602299f 100644 --- a/Wino.Calendar/Views/AppShell.xaml +++ b/Wino.Calendar/Views/AppShell.xaml @@ -89,6 +89,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Center" BorderBrush="Transparent" + IsTabStop="False" PlaceholderText="Search" /> /// The type of calendar item (Timed, AllDay, MultiDay, etc.) /// - public CalendarItemType ItemType { get; set; } = CalendarItemType.Timed; + public CalendarItemType ItemType { get; set; } /// /// Automatically determines and sets the ItemType based on event properties diff --git a/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs b/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs index 3eaf1ff7..8d7b099d 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarItemViewModel.cs @@ -1,9 +1,13 @@ -namespace Wino.Core.Domain.Interfaces; +using System; + +namespace Wino.Core.Domain.Interfaces; /// /// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection. /// -public interface ICalendarItemViewModel +public interface ICalendarItemViewModel : ICalendarItem { bool IsSelected { get; set; } + DateTime LocalStartDateTime { get; } + DateTime LocalEndDateTime { get; } } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 48508566..cfc5e400 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -1861,7 +1861,8 @@ public class OutlookSynchronizer : WinoSynchronizer().ToListAsync(); } - public async Task GetEventByRemoteIdAsync(string remoteEventId) + public async Task GetEventByRemoteIdAsync(string remoteEventId) { return await Connection.Table() .Where(e => e.RemoteEventId == remoteEventId && !e.IsDeleted) @@ -121,7 +123,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx .ToListAsync(); } - public async Task GetCalendarByRemoteIdAsync(string remoteCalendarId) + public async Task GetCalendarByRemoteIdAsync(string remoteCalendarId) { return await Connection.Table() .Where(c => c.RemoteCalendarId == remoteCalendarId && !c.IsDeleted) @@ -324,7 +326,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx /// /// The RRULE string (without RRULE: prefix) /// Dictionary of rule parameters - private Dictionary? ParseRRule(string rrule) + private Dictionary ParseRRule(string rrule) { try { @@ -408,7 +410,9 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx LastModified = originalEvent.LastModified, IsDeleted = false, RecurringEventId = originalEvent.RemoteEventId, - OriginalStartTime = instanceStart.ToString("O") + OriginalStartTime = instanceStart.ToString("O"), + HtmlLink = originalEvent.HtmlLink, + ItemType = originalEvent.ItemType }; } @@ -522,42 +526,56 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx public async Task> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar) { var allEvents = new List(); - + var gg = Guid.Parse("c10b83b0-9423-4d26-82d3-34b63d2e1297"); + var tt = await Connection.Table().Where(a => a.Id == gg).FirstOrDefaultAsync(); + var type = tt.ItemType; // Get all non-recurring events in the date range - var oneTimeEvents = await Connection.Table() - .Where(e => !e.IsDeleted && - (string.IsNullOrEmpty(e.RecurrenceRules) || e.RecurrenceRules == "") && - string.IsNullOrEmpty(e.RecurringEventId) && // Ensure it's not a modified instance - e.StartDateTime >= startDate && e.StartDateTime <= endDate - && e.CalendarId == calendar.Id) - .ToListAsync(); + var oneTimeEventsQuery = new Query() + .From(nameof(CalendarItem)) + .Where(nameof(CalendarItem.IsDeleted), false) + .Where(q => q.WhereNull(nameof(CalendarItem.RecurrenceRules)).OrWhere(nameof(CalendarItem.RecurrenceRules), "")) + .Where(q => q.WhereNull(nameof(CalendarItem.RecurringEventId)).OrWhere(nameof(CalendarItem.RecurringEventId), "")) + .Where(nameof(CalendarItem.StartDateTime), ">=", startDate) + .Where(nameof(CalendarItem.StartDateTime), "<=", endDate) + .Where(nameof(CalendarItem.CalendarId), calendar.Id); + var oneTimeEvents = await Connection.QueryAsync(oneTimeEventsQuery.GetRawQuery()); allEvents.AddRange(oneTimeEvents); // Get all recurring events (master events only) - var recurringEvents = await Connection.Table() - .Where(e => !e.IsDeleted && - !string.IsNullOrEmpty(e.RecurrenceRules) && - e.RecurrenceRules != "" && - string.IsNullOrEmpty(e.RecurringEventId) && - e.CalendarId == calendar.Id) // Master events, not instances - .ToListAsync(); + var recurringEventsQuery = new Query() + .From(nameof(CalendarItem)) + .Where(nameof(CalendarItem.IsDeleted), false) + .WhereNotNull(nameof(CalendarItem.RecurrenceRules)) + .Where(nameof(CalendarItem.RecurrenceRules), "!=", "") + .Where(q => q.WhereNull(nameof(CalendarItem.RecurringEventId)).OrWhere(nameof(CalendarItem.RecurringEventId), "")) + .Where(nameof(CalendarItem.CalendarId), calendar.Id); + + var recurringEvents = await Connection.QueryAsync(recurringEventsQuery.GetRawQuery()); // Get all exception instances (modified or moved instances) - var exceptionInstances = await Connection.Table() - .Where(e => !e.IsDeleted && - e.CalendarId == calendar.Id && - !string.IsNullOrEmpty(e.RecurringEventId) && - e.StartDateTime >= startDate && e.StartDateTime <= endDate) - .ToListAsync(); + var exceptionInstancesQuery = new Query() + .From(nameof(CalendarItem)) + .Where(nameof(CalendarItem.IsDeleted), false) + .Where(nameof(CalendarItem.CalendarId), calendar.Id) + .WhereNotNull(nameof(CalendarItem.RecurringEventId)) + .Where(nameof(CalendarItem.RecurringEventId), "!=", "") + .Where(nameof(CalendarItem.StartDateTime), ">=", startDate) + .Where(nameof(CalendarItem.StartDateTime), "<=", endDate); + + var exceptionInstances = await Connection.QueryAsync(exceptionInstancesQuery.GetRawQuery()); // Get all canceled instances (marked as deleted but with RecurringEventId) - var canceledInstances = await Connection.Table() - .Where(e => e.IsDeleted && - e.CalendarId == calendar.Id && - !string.IsNullOrEmpty(e.RecurringEventId) && - !string.IsNullOrEmpty(e.OriginalStartTime)) - .ToListAsync(); + var canceledInstancesQuery = new Query() + .From(nameof(CalendarItem)) + .Where(nameof(CalendarItem.IsDeleted), true) + .Where(nameof(CalendarItem.CalendarId), calendar.Id) + .WhereNotNull(nameof(CalendarItem.RecurringEventId)) + .Where(nameof(CalendarItem.RecurringEventId), "!=", "") + .WhereNotNull(nameof(CalendarItem.OriginalStartTime)) + .Where(nameof(CalendarItem.OriginalStartTime), "!=", ""); + + var canceledInstances = await Connection.QueryAsync(canceledInstancesQuery.GetRawQuery()); // Group exceptions and cancellations by their parent recurring event var exceptionsByParent = exceptionInstances @@ -583,6 +601,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx // Add the exception instances (modified/moved instances) to the final list allEvents.AddRange(exceptionInstances); + // allEvents[0].DetermineItemType(); // Sort by start date and return return allEvents.OrderBy(e => e.StartDateTime).ToList(); } @@ -765,7 +784,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx /// /// The calendar ID /// The sync token or null if not found - public async Task GetCalendarSyncTokenAsync(string calendarId) + public async Task GetCalendarSyncTokenAsync(string calendarId) { var calendar = await GetCalendarByRemoteIdAsync(calendarId); return calendar?.SynchronizationDeltaToken;