Event details UI improvements.

This commit is contained in:
Burak Kaan Köse
2026-01-01 10:07:56 +01:00
parent e71c050724
commit 3b485dc1fe
14 changed files with 311 additions and 113 deletions
@@ -817,15 +817,10 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
private IEnumerable<CalendarItemViewModel> GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay)
{
// All-day and multi-day events are selected collectively.
// 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.
// Multi-day events, all-day events, and recurring events are rendered across multiple days.
// We need to find all instances with the same ID across all visible date ranges.
if (!calendarItemViewModel.IsRecurringEvent)
{
return [calendarItemViewModel];
}
else
if (calendarItemViewModel.IsRecurringEvent || calendarItemViewModel.IsMultiDayEvent)
{
return DayRanges
.SelectMany(a => a.CalendarDays)
@@ -834,6 +829,11 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
.Cast<CalendarItemViewModel>()
.Distinct();
}
else
{
// Single-day, non-recurring events only appear once
return [calendarItemViewModel];
}
}
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
@@ -877,6 +877,13 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
Debug.WriteLine($"Calendar item deleted: {calendarItem.Id}");
// Check if the deleted item is currently displayed in details view
if (DisplayDetailsCalendarItemViewModel?.Id == calendarItem.Id)
{
// Clear the details view since this item was deleted
DisplayDetailsCalendarItemViewModel = null;
}
// Remove the event and its occurrences from all visible date ranges.
await ExecuteUIThread(() =>
{
@@ -956,6 +963,47 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Check if event falls into the current date range.
if (DayRanges.DisplayRange == null) return;
// Check if this is a server-synced item that matches a local preview
// Local previews don't have RemoteEventId, server-synced items do
if (!string.IsNullOrEmpty(calendarItem.RemoteEventId))
{
// Find local preview items that match this event's properties
var localPreviewItems = DayRanges
.SelectMany(a => a.CalendarDays)
.SelectMany(b => b.EventsCollection.RegularEvents.Concat(b.EventsCollection.AllDayEvents))
.OfType<CalendarItemViewModel>()
.Where(c => c.AssignedCalendar.Id == calendarItem.CalendarId &&
c.CalendarItem.IsLocalPreview && // Local preview (no RemoteEventId)
c.Title == calendarItem.Title &&
Math.Abs((c.StartDate - calendarItem.LocalStartDate).TotalSeconds) < 60 &&
Math.Abs(c.DurationInSeconds - calendarItem.DurationInSeconds) < 1)
.ToList();
if (localPreviewItems.Any())
{
Debug.WriteLine($"Found {localPreviewItems.Count} matching local preview items for {calendarItem.Title}, removing them.");
// Remove all matching local preview items
await ExecuteUIThread(() =>
{
foreach (var dayRange in DayRanges)
{
foreach (var calendarDay in dayRange.CalendarDays)
{
foreach (var localPreview in localPreviewItems)
{
var itemInDay = calendarDay.EventsCollection.GetCalendarItem(localPreview.Id);
if (itemInDay != null)
{
calendarDay.EventsCollection.RemoveCalendarItem(itemInDay);
}
}
}
}
});
}
}
// Get all periods from the visible day ranges
var visiblePeriods = DayRanges.Select(dr => dr.Period).ToList();
@@ -18,54 +18,51 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
/// <summary>
/// Gets or sets the start date in local time based on the event's timezone.
/// The underlying CalendarItem stores dates in UTC.
/// Gets or sets the start date converted to user's local timezone for display.
/// The underlying CalendarItem stores dates according to their timezone.
/// </summary>
public DateTime StartDate
{
get
{
// Convert from UTC stored in database to local time using the event's timezone
var startDateTimeOffset = CalendarItem.StartDateTimeOffset;
return startDateTimeOffset.LocalDateTime;
// Get start date in user's local timezone
return CalendarItem.LocalStartDate;
}
set
{
// When setting, convert from local time to UTC for storage
// Preserve the timezone information
// When setting from UI (in local time), convert to event's timezone for storage
if (!string.IsNullOrEmpty(CalendarItem.StartTimeZone))
{
try
{
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.StartTimeZone);
var utcDateTime = TimeZoneInfo.ConvertTimeToUtc(value, timeZoneInfo);
CalendarItem.StartDate = utcDateTime;
var sourceTimeZone = TimeZoneInfo.Local;
var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.StartTimeZone);
CalendarItem.StartDate = TimeZoneInfo.ConvertTime(value, sourceTimeZone, targetTimeZone);
}
catch
{
// If timezone lookup fails, assume value is already in UTC
// If timezone lookup fails, set as-is
CalendarItem.StartDate = value;
}
}
else
{
// No timezone info, assume UTC
// No timezone info, set as-is
CalendarItem.StartDate = value;
}
}
}
/// <summary>
/// Gets the end date in local time based on the event's timezone.
/// The underlying CalendarItem stores dates in UTC.
/// Gets the end date converted to user's local timezone for display.
/// The underlying CalendarItem stores dates according to their timezone.
/// </summary>
public DateTime EndDate
{
get
{
// Convert from UTC stored in database to local time using the event's timezone
var endDateTimeOffset = CalendarItem.EndDateTimeOffset;
return endDateTimeOffset.LocalDateTime;
// Get end date in user's local timezone
return CalendarItem.LocalEndDate;
}
}
@@ -100,4 +97,4 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
}
public override string ToString() => CalendarItem.Title;
}
}
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
@@ -19,6 +21,9 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
private readonly ICalendarService _calendarService;
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
private readonly IMailDialogService _dialogService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly INavigationService _navigationService;
public CalendarSettings CurrentSettings { get; }
@@ -26,20 +31,55 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
[NotifyPropertyChangedFor(nameof(CanEditSeries))]
private CalendarItemViewModel _currentEvent;
[ObservableProperty]
private CalendarItemViewModel _seriesParent;
/// <summary>
/// Returns true if the event is part of a recurring series (as a child occurrence).
/// Used to enable "View Series" functionality.
/// </summary>
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
/// <summary>
/// Returns true if the "Edit Series" button should be visible.
/// Only visible for child occurrences of recurring events, not for master events or single events.
/// </summary>
public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false;
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
#region Show As Options
public List<CalendarItemShowAs> ShowAsOptions { get; } =
[
CalendarItemShowAs.Free,
CalendarItemShowAs.Tentative,
CalendarItemShowAs.Busy,
CalendarItemShowAs.OutOfOffice,
CalendarItemShowAs.WorkingElsewhere
];
[ObservableProperty]
public partial CalendarItemShowAs SelectedShowAs { get; set; } = CalendarItemShowAs.Busy;
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService,
INativeAppService nativeAppService,
IPreferencesService preferencesService,
IMailDialogService dialogService,
IWinoRequestDelegator winoRequestDelegator,
INavigationService navigationService)
{
_calendarService = calendarService;
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
_dialogService = dialogService;
_winoRequestDelegator = winoRequestDelegator;
_navigationService = navigationService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
}
@@ -90,26 +130,57 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
[RelayCommand]
private async Task SaveAsync()
{
// TODO: Implement saving
}
[RelayCommand]
private async Task DeleteAsync()
{
if (CurrentEvent == null) return;
// If the event is a master recurring event, ask for confirmation
if (CurrentEvent.IsRecurringParent)
{
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
Translator.DialogMessage_DeleteRecurringSeriesMessage,
Translator.DialogMessage_DeleteRecurringSeriesTitle,
Translator.Buttons_Delete);
if (!confirmed) return;
}
try
{
var preparationRequest = new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.DeleteEvent,
CurrentEvent.CalendarItem,
null);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
// Navigate back after successful deletion
_navigationService.GoBack();
}
catch (Exception ex)
{
Debug.WriteLine($"Error deleting calendar event: {ex.Message}");
}
}
[RelayCommand]
private Task JoinOnline()
private Task JoinOnlineAsync()
{
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask;
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink))
return Task.CompletedTask;
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
}
[RelayCommand]
private async Task Respond(CalendarItemStatus status)
private async Task RespondAsync(CalendarItemStatus status)
{
if (CurrentEvent == null) return;
// TODO: Implement response
}
}