Local time rendering for events.

This commit is contained in:
Burak Kaan Köse
2025-07-07 21:03:07 +02:00
parent 1ee0063b62
commit 03c9ac1e11
9 changed files with 136 additions and 41 deletions

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -141,6 +142,16 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
Type = CalendarSynchronizationType.CalendarMetadata Type = CalendarSynchronizationType.CalendarMetadata
}; };
var timer = new Stopwatch();
var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client)); var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
timer.Stop();
Debug.WriteLine("Synchronization completed in {timer.ElapsedMilliseconds} ms");
// TODO: Properly handle synchronization errors.
accountCreationDialog.Complete(false);
} }
} }

View File

@@ -789,6 +789,13 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Recurring events must be selected as a single instance. // 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. // We need to find the day that the event is in, and then select the event.
if (calendarItemViewModel == null) return Enumerable.Empty<CalendarItemViewModel>();
// If the calendar item is not recurring, we can just return it.
if (calendarItemViewModel.ItemType == CalendarItemType.Timed) return new[] { calendarItemViewModel };
// TODO: Implement below logic. // TODO: Implement below logic.
return default; return default;
@@ -825,6 +832,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay); var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
if (itemsToSelect == null) return;
foreach (var item in itemsToSelect) foreach (var item in itemsToSelect)
{ {
item.IsSelected = true; item.IsSelected = true;

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod; using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
@@ -22,6 +23,16 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public DateTime EndDateTime => CalendarItem.EndDateTime; public DateTime EndDateTime => CalendarItem.EndDateTime;
/// <summary>
/// Gets the start date and time in the local time zone for display purposes.
/// </summary>
public DateTime LocalStartDateTime => ConvertToLocalTime();
/// <summary>
/// Gets the end date and time in the local time zone for display purposes.
/// </summary>
public DateTime LocalEndDateTime => ConvertToLocalTime();
public ITimePeriod Period => CalendarItem.Period; public ITimePeriod Period => CalendarItem.Period;
public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.RecurrenceRules) || !string.IsNullOrEmpty(CalendarItem.RecurringEventId); public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.RecurrenceRules) || !string.IsNullOrEmpty(CalendarItem.RecurringEventId);
@@ -31,11 +42,50 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>(); public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemType ItemType => ((ICalendarItem)CalendarItem).ItemType; public CalendarItemType ItemType => CalendarItem.ItemType;
public CalendarItemViewModel(CalendarItem calendarItem) public CalendarItemViewModel(CalendarItem calendarItem)
{ {
CalendarItem = calendarItem; CalendarItem = calendarItem;
Debug.WriteLine($"{Title} : {ItemType}");
}
/// <summary>
/// Converts a DateTime to local time based on the provided timezone.
/// If timezone is empty or null, assumes the DateTime is in UTC.
/// </summary>
/// <param name="dateTime">The DateTime to convert</param>
/// <param name="timeZone">The timezone string. If empty/null, assumes UTC.</param>
/// <returns>DateTime converted to local time</returns>
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; public override string ToString() => CalendarItem.Title;

View File

@@ -45,9 +45,9 @@ public partial class WinoCalendarPanel : Panel
private void ResetMeasurements() => _measurements.Clear(); 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) if (childStart <= Period.Start)
{ {
@@ -69,7 +69,7 @@ public partial class WinoCalendarPanel : Panel
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth) private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
=> availableWidth * calendarItemMeasurement.Left; => availableWidth * calendarItemMeasurement.Left;
private double GetChildHeight(ICalendarItem child) private double GetChildHeight(ICalendarItemViewModel child)
{ {
// All day events are not measured. // All day events are not measured.
if (child.ItemType == CalendarItemType.AllDay) return 0; if (child.ItemType == CalendarItemType.AllDay) return 0;
@@ -128,7 +128,7 @@ public partial class WinoCalendarPanel : Panel
foreach (var control in calendarControls) foreach (var control in calendarControls)
{ {
// We can't arrange this child. // We can't arrange this child.
if (!(control.Content is ICalendarItem child)) continue; if (!(control.Content is ICalendarItemViewModel child)) continue;
bool isHorizontallyLastItem = false; bool isHorizontallyLastItem = false;

View File

@@ -89,6 +89,7 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
BorderBrush="Transparent" BorderBrush="Transparent"
IsTabStop="False"
PlaceholderText="Search" /> PlaceholderText="Search" />
<StackPanel <StackPanel

View File

@@ -71,7 +71,7 @@ public class CalendarItem : ICalendarItem
/// <summary> /// <summary>
/// The type of calendar item (Timed, AllDay, MultiDay, etc.) /// The type of calendar item (Timed, AllDay, MultiDay, etc.)
/// </summary> /// </summary>
public CalendarItemType ItemType { get; set; } = CalendarItemType.Timed; public CalendarItemType ItemType { get; set; }
/// <summary> /// <summary>
/// Automatically determines and sets the ItemType based on event properties /// Automatically determines and sets the ItemType based on event properties

View File

@@ -1,9 +1,13 @@
namespace Wino.Core.Domain.Interfaces; using System;
namespace Wino.Core.Domain.Interfaces;
/// <summary> /// <summary>
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection. /// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
/// </summary> /// </summary>
public interface ICalendarItemViewModel public interface ICalendarItemViewModel : ICalendarItem
{ {
bool IsSelected { get; set; } bool IsSelected { get; set; }
DateTime LocalStartDateTime { get; }
DateTime LocalEndDateTime { get; }
} }

View File

@@ -1861,7 +1861,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
RecurrenceRules = FormatRecurrence(outlookEvent.Recurrence), RecurrenceRules = FormatRecurrence(outlookEvent.Recurrence),
Status = outlookEvent.IsCancelled == true ? "cancelled" : "confirmed", Status = outlookEvent.IsCancelled == true ? "cancelled" : "confirmed",
IsDeleted = outlookEvent.IsCancelled == true, IsDeleted = outlookEvent.IsCancelled == true,
LastModified = DateTime.UtcNow LastModified = DateTime.UtcNow,
TimeZone = outlookEvent.Start.TimeZone
}; };
// Automatically determine the calendar item type based on event properties // Automatically determine the calendar item type based on event properties

View File

@@ -2,9 +2,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using SqlKata;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Services.Extensions;
namespace Wino.Services; namespace Wino.Services;
@@ -28,7 +30,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx
return await Connection.Table<CalendarItem>().ToListAsync(); return await Connection.Table<CalendarItem>().ToListAsync();
} }
public async Task<CalendarItem?> GetEventByRemoteIdAsync(string remoteEventId) public async Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId)
{ {
return await Connection.Table<CalendarItem>() return await Connection.Table<CalendarItem>()
.Where(e => e.RemoteEventId == remoteEventId && !e.IsDeleted) .Where(e => e.RemoteEventId == remoteEventId && !e.IsDeleted)
@@ -121,7 +123,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx
.ToListAsync(); .ToListAsync();
} }
public async Task<AccountCalendar?> GetCalendarByRemoteIdAsync(string remoteCalendarId) public async Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId)
{ {
return await Connection.Table<AccountCalendar>() return await Connection.Table<AccountCalendar>()
.Where(c => c.RemoteCalendarId == remoteCalendarId && !c.IsDeleted) .Where(c => c.RemoteCalendarId == remoteCalendarId && !c.IsDeleted)
@@ -324,7 +326,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx
/// </summary> /// </summary>
/// <param name="rrule">The RRULE string (without RRULE: prefix)</param> /// <param name="rrule">The RRULE string (without RRULE: prefix)</param>
/// <returns>Dictionary of rule parameters</returns> /// <returns>Dictionary of rule parameters</returns>
private Dictionary<string, string>? ParseRRule(string rrule) private Dictionary<string, string> ParseRRule(string rrule)
{ {
try try
{ {
@@ -408,7 +410,9 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx
LastModified = originalEvent.LastModified, LastModified = originalEvent.LastModified,
IsDeleted = false, IsDeleted = false,
RecurringEventId = originalEvent.RemoteEventId, 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<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar) public async Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar)
{ {
var allEvents = new List<CalendarItem>(); var allEvents = new List<CalendarItem>();
var gg = Guid.Parse("c10b83b0-9423-4d26-82d3-34b63d2e1297");
var tt = await Connection.Table<CalendarItem>().Where(a => a.Id == gg).FirstOrDefaultAsync();
var type = tt.ItemType;
// Get all non-recurring events in the date range // Get all non-recurring events in the date range
var oneTimeEvents = await Connection.Table<CalendarItem>() var oneTimeEventsQuery = new Query()
.Where(e => !e.IsDeleted && .From(nameof(CalendarItem))
(string.IsNullOrEmpty(e.RecurrenceRules) || e.RecurrenceRules == "") && .Where(nameof(CalendarItem.IsDeleted), false)
string.IsNullOrEmpty(e.RecurringEventId) && // Ensure it's not a modified instance .Where(q => q.WhereNull(nameof(CalendarItem.RecurrenceRules)).OrWhere(nameof(CalendarItem.RecurrenceRules), ""))
e.StartDateTime >= startDate && e.StartDateTime <= endDate .Where(q => q.WhereNull(nameof(CalendarItem.RecurringEventId)).OrWhere(nameof(CalendarItem.RecurringEventId), ""))
&& e.CalendarId == calendar.Id) .Where(nameof(CalendarItem.StartDateTime), ">=", startDate)
.ToListAsync(); .Where(nameof(CalendarItem.StartDateTime), "<=", endDate)
.Where(nameof(CalendarItem.CalendarId), calendar.Id);
var oneTimeEvents = await Connection.QueryAsync<CalendarItem>(oneTimeEventsQuery.GetRawQuery());
allEvents.AddRange(oneTimeEvents); allEvents.AddRange(oneTimeEvents);
// Get all recurring events (master events only) // Get all recurring events (master events only)
var recurringEvents = await Connection.Table<CalendarItem>() var recurringEventsQuery = new Query()
.Where(e => !e.IsDeleted && .From(nameof(CalendarItem))
!string.IsNullOrEmpty(e.RecurrenceRules) && .Where(nameof(CalendarItem.IsDeleted), false)
e.RecurrenceRules != "" && .WhereNotNull(nameof(CalendarItem.RecurrenceRules))
string.IsNullOrEmpty(e.RecurringEventId) && .Where(nameof(CalendarItem.RecurrenceRules), "!=", "")
e.CalendarId == calendar.Id) // Master events, not instances .Where(q => q.WhereNull(nameof(CalendarItem.RecurringEventId)).OrWhere(nameof(CalendarItem.RecurringEventId), ""))
.ToListAsync(); .Where(nameof(CalendarItem.CalendarId), calendar.Id);
var recurringEvents = await Connection.QueryAsync<CalendarItem>(recurringEventsQuery.GetRawQuery());
// Get all exception instances (modified or moved instances) // Get all exception instances (modified or moved instances)
var exceptionInstances = await Connection.Table<CalendarItem>() var exceptionInstancesQuery = new Query()
.Where(e => !e.IsDeleted && .From(nameof(CalendarItem))
e.CalendarId == calendar.Id && .Where(nameof(CalendarItem.IsDeleted), false)
!string.IsNullOrEmpty(e.RecurringEventId) && .Where(nameof(CalendarItem.CalendarId), calendar.Id)
e.StartDateTime >= startDate && e.StartDateTime <= endDate) .WhereNotNull(nameof(CalendarItem.RecurringEventId))
.ToListAsync(); .Where(nameof(CalendarItem.RecurringEventId), "!=", "")
.Where(nameof(CalendarItem.StartDateTime), ">=", startDate)
.Where(nameof(CalendarItem.StartDateTime), "<=", endDate);
var exceptionInstances = await Connection.QueryAsync<CalendarItem>(exceptionInstancesQuery.GetRawQuery());
// Get all canceled instances (marked as deleted but with RecurringEventId) // Get all canceled instances (marked as deleted but with RecurringEventId)
var canceledInstances = await Connection.Table<CalendarItem>() var canceledInstancesQuery = new Query()
.Where(e => e.IsDeleted && .From(nameof(CalendarItem))
e.CalendarId == calendar.Id && .Where(nameof(CalendarItem.IsDeleted), true)
!string.IsNullOrEmpty(e.RecurringEventId) && .Where(nameof(CalendarItem.CalendarId), calendar.Id)
!string.IsNullOrEmpty(e.OriginalStartTime)) .WhereNotNull(nameof(CalendarItem.RecurringEventId))
.ToListAsync(); .Where(nameof(CalendarItem.RecurringEventId), "!=", "")
.WhereNotNull(nameof(CalendarItem.OriginalStartTime))
.Where(nameof(CalendarItem.OriginalStartTime), "!=", "");
var canceledInstances = await Connection.QueryAsync<CalendarItem>(canceledInstancesQuery.GetRawQuery());
// Group exceptions and cancellations by their parent recurring event // Group exceptions and cancellations by their parent recurring event
var exceptionsByParent = exceptionInstances var exceptionsByParent = exceptionInstances
@@ -583,6 +601,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx
// Add the exception instances (modified/moved instances) to the final list // Add the exception instances (modified/moved instances) to the final list
allEvents.AddRange(exceptionInstances); allEvents.AddRange(exceptionInstances);
// allEvents[0].DetermineItemType();
// Sort by start date and return // Sort by start date and return
return allEvents.OrderBy(e => e.StartDateTime).ToList(); return allEvents.OrderBy(e => e.StartDateTime).ToList();
} }
@@ -765,7 +784,7 @@ public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx
/// </summary> /// </summary>
/// <param name="calendarId">The calendar ID</param> /// <param name="calendarId">The calendar ID</param>
/// <returns>The sync token or null if not found</returns> /// <returns>The sync token or null if not found</returns>
public async Task<string?> GetCalendarSyncTokenAsync(string calendarId) public async Task<string> GetCalendarSyncTokenAsync(string calendarId)
{ {
var calendar = await GetCalendarByRemoteIdAsync(calendarId); var calendar = await GetCalendarByRemoteIdAsync(calendarId);
return calendar?.SynchronizationDeltaToken; return calendar?.SynchronizationDeltaToken;