From 3dc4ac03ec0bbe642c1759bb40c865c91780a86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 8 Apr 2026 23:46:02 +0200 Subject: [PATCH] Initial feature for drag / drop calendar events. --- .../CalendarPageViewModel.cs | 97 ++++++ .../Data/AccountCalendarViewModel.cs | 6 +- .../Data/CalendarItemViewModel.cs | 2 + .../Entities/Calendar/CalendarItem.cs | 18 ++ Wino.Core.Domain/Enums/MailOperation.cs | 1 + .../CalendarOperationPreparationRequest.cs | 2 +- .../Translations/en_US/resources.json | 2 + Wino.Core.Tests/CalendarPageViewModelTests.cs | 24 ++ .../WinoSynchronizerCalendarRequestTests.cs | 28 ++ .../Calendar/ChangeStartAndEndDateRequest.cs | 15 + Wino.Core/Services/WinoRequestDelegator.cs | 5 + Wino.Core/Synchronizers/GmailSynchronizer.cs | 3 + Wino.Core/Synchronizers/ImapSynchronizer.cs | 9 + .../Synchronizers/OutlookSynchronizer.cs | 3 + Wino.Core/Synchronizers/WinoSynchronizer.cs | 6 + Wino.Mail.WinUI/AppThemes/Acrylic.xaml | 2 + Wino.Mail.WinUI/AppThemes/Clouds.xaml | 2 + Wino.Mail.WinUI/AppThemes/Custom.xaml | 2 + Wino.Mail.WinUI/AppThemes/Default.xaml | 2 + Wino.Mail.WinUI/AppThemes/Forest.xaml | 2 + Wino.Mail.WinUI/AppThemes/Garden.xaml | 2 + Wino.Mail.WinUI/AppThemes/Nighty.xaml | 2 + Wino.Mail.WinUI/AppThemes/Snowflake.xaml | 2 + .../Controls/Calendar/CalendarDragPackage.cs | 13 + .../Calendar/CalendarItemControl.xaml.cs | 23 +- .../Calendar/CalendarItemDroppedEventArgs.cs | 28 ++ .../Calendar/CalendarPeriodControl.xaml | 20 +- .../Calendar/CalendarPeriodControl.xaml.cs | 299 ++++++++++++++++++ .../Views/Calendar/CalendarPage.xaml | 2 + .../Views/Calendar/CalendarPage.xaml.cs | 3 + 30 files changed, 621 insertions(+), 4 deletions(-) create mode 100644 Wino.Core/Requests/Calendar/ChangeStartAndEndDateRequest.cs create mode 100644 Wino.Mail.WinUI/Controls/Calendar/CalendarDragPackage.cs create mode 100644 Wino.Mail.WinUI/Controls/Calendar/CalendarItemDroppedEventArgs.cs diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index abce683e..087e76d2 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -526,6 +526,59 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, SelectedEndTimeString = CurrentSettings.GetTimeString(endTime); } + public async Task MoveCalendarItemAsync(CalendarItemViewModel calendarItemViewModel, DateTime targetStart) + { + if (calendarItemViewModel?.CalendarItem == null) + { + return; + } + + var calendarItem = calendarItemViewModel.CalendarItem; + + if (!calendarItem.CanChangeStartAndEndDate) + { + _dialogService.InfoBarMessage( + Translator.CalendarDragDropMoveNotAllowedTitle, + Translator.CalendarDragDropMoveNotAllowedMessage, + InfoBarMessageType.Warning); + return; + } + + var normalizedTargetStart = calendarItem.IsAllDayEvent + ? targetStart.Date + : targetStart; + var targetEnd = normalizedTargetStart.AddSeconds(calendarItem.DurationInSeconds); + var currentLocalStart = calendarItem.LocalStartDate; + var currentLocalEnd = calendarItem.LocalEndDate; + + if (currentLocalStart == normalizedTargetStart && currentLocalEnd == targetEnd) + { + return; + } + + var originalItem = CloneCalendarItem(calendarItem); + var attendees = await _calendarService.GetAttendeesAsync(calendarItem.EventTrackingId).ConfigureAwait(false) ?? []; + var originalAttendees = CloneAttendees(attendees); + + await ExecuteUIThread(() => + { + calendarItemViewModel.StartDate = normalizedTargetStart; + calendarItemViewModel.DurationInSeconds = calendarItem.DurationInSeconds; + }).ConfigureAwait(false); + + await _calendarService.UpdateCalendarItemAsync(calendarItem, attendees).ConfigureAwait(false); + + var preparationRequest = new CalendarOperationPreparationRequest( + CalendarSynchronizerOperation.ChangeStartAndEndDate, + calendarItem, + attendees, + ResponseMessage: null, + OriginalItem: originalItem, + OriginalAttendees: originalAttendees); + + await _winoRequestDelegator.ExecuteAsync(preparationRequest).ConfigureAwait(false); + } + partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value) => DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty); @@ -872,6 +925,50 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, source == EntityUpdateSource.ClientUpdated ? new HashSet { calendarItem.Id } : null, source); + private static CalendarItem CloneCalendarItem(CalendarItem calendarItem) + => new() + { + Id = calendarItem.Id, + RemoteEventId = calendarItem.RemoteEventId, + Title = calendarItem.Title, + Description = calendarItem.Description, + Location = calendarItem.Location, + StartDate = calendarItem.StartDate, + StartTimeZone = calendarItem.StartTimeZone, + EndTimeZone = calendarItem.EndTimeZone, + DurationInSeconds = calendarItem.DurationInSeconds, + Recurrence = calendarItem.Recurrence, + OrganizerDisplayName = calendarItem.OrganizerDisplayName, + OrganizerEmail = calendarItem.OrganizerEmail, + RecurringCalendarItemId = calendarItem.RecurringCalendarItemId, + IsLocked = calendarItem.IsLocked, + IsHidden = calendarItem.IsHidden, + CustomEventColorHex = calendarItem.CustomEventColorHex, + HtmlLink = calendarItem.HtmlLink, + SnoozedUntil = calendarItem.SnoozedUntil, + Status = calendarItem.Status, + Visibility = calendarItem.Visibility, + ShowAs = calendarItem.ShowAs, + CreatedAt = calendarItem.CreatedAt, + UpdatedAt = calendarItem.UpdatedAt, + CalendarId = calendarItem.CalendarId, + AssignedCalendar = calendarItem.AssignedCalendar + }; + + private static List CloneAttendees(IEnumerable attendees) + => attendees?.Select(attendee => new CalendarEventAttendee + { + Id = attendee.Id, + CalendarItemId = attendee.CalendarItemId, + Name = attendee.Name, + Email = attendee.Email, + AttendenceStatus = attendee.AttendenceStatus, + IsOrganizer = attendee.IsOrganizer, + IsOptionalAttendee = attendee.IsOptionalAttendee, + Comment = attendee.Comment, + ResolvedContact = attendee.ResolvedContact + }).ToList() ?? []; + private CalendarItemViewModel CreateCalendarItemViewModel(CalendarItem calendarItem, ISet pendingCalendarItemIds, EntityUpdateSource source = EntityUpdateSource.Server) { calendarItem.AssignedCalendar ??= ResolveAssignedCalendar(calendarItem.CalendarId); diff --git a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs index 1f41f633..afdd6699 100644 --- a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs @@ -79,5 +79,9 @@ public partial class AccountCalendarViewModel : ObservableObject, IAccountCalend set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s); } public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; } - public MailAccount MailAccount { get => MailAccount; set => MailAccount = value; } + public MailAccount MailAccount + { + get => AccountCalendar.MailAccount ?? Account; + set => AccountCalendar.MailAccount = value; + } } diff --git a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs index de2416a6..2fa494b9 100644 --- a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs @@ -72,6 +72,7 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC public bool IsRecurringEvent => CalendarItem.IsRecurringEvent; public bool IsRecurringChild => CalendarItem.IsRecurringChild; public bool IsRecurringParent => CalendarItem.IsRecurringParent; + public bool CanDragDrop => CalendarItem.CanChangeStartAndEndDate; [ObservableProperty] public partial bool IsSelected { get; set; } @@ -157,6 +158,7 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC OnPropertyChanged(nameof(IsRecurringEvent)); OnPropertyChanged(nameof(IsRecurringChild)); OnPropertyChanged(nameof(IsRecurringParent)); + OnPropertyChanged(nameof(CanDragDrop)); OnPropertyChanged(nameof(AssignedCalendar)); OnPropertyChanged(nameof(DisplayTitle)); } diff --git a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs index a04576f9..cdd64c42 100644 --- a/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs +++ b/Wino.Core.Domain/Entities/Calendar/CalendarItem.cs @@ -154,6 +154,24 @@ public class CalendarItem : ICalendarItem [Ignore] public IAccountCalendar AssignedCalendar { get; set; } + [Ignore] + public bool CanChangeStartAndEndDate + { + get + { + if (IsLocked) + { + return false; + } + + var accountAddress = AssignedCalendar?.MailAccount?.Address; + + return string.IsNullOrWhiteSpace(OrganizerEmail) || + string.IsNullOrWhiteSpace(accountAddress) || + string.Equals(OrganizerEmail, accountAddress, StringComparison.OrdinalIgnoreCase); + } + } + /// /// Id to load information related to this event (attendees, reminders, etc.). /// For child events, if they have their own data, use their own Id. diff --git a/Wino.Core.Domain/Enums/MailOperation.cs b/Wino.Core.Domain/Enums/MailOperation.cs index 1fb303c3..3f34c2fa 100644 --- a/Wino.Core.Domain/Enums/MailOperation.cs +++ b/Wino.Core.Domain/Enums/MailOperation.cs @@ -28,6 +28,7 @@ public enum CalendarSynchronizerOperation { CreateEvent, UpdateEvent, + ChangeStartAndEndDate, DeleteEvent, AcceptEvent, DeclineEvent, diff --git a/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs b/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs index a43c6e10..54b6d681 100644 --- a/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs +++ b/Wino.Core.Domain/Models/Calendar/CalendarOperationPreparationRequest.cs @@ -7,7 +7,7 @@ namespace Wino.Core.Domain.Models.Calendar; /// /// Encapsulates the options for preparing calendar operation requests. /// -/// Calendar operation to execute (Create, Update, Delete, Accept, Decline, Tentative). +/// Calendar operation to execute (Create, Update, ChangeStartAndEndDate, Delete, Accept, Decline, Tentative). /// Calendar item to operate on. /// List of attendees for the calendar event. /// Optional message to include with event responses (Accept, Decline, Tentative). diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 902f8bef..60b34cb6 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -210,6 +210,8 @@ "CalendarItem_DetailsPopup_JoinOnline": "Join online", "CalendarItem_DetailsPopup_ViewEventButton": "View event", "CalendarItem_DetailsPopup_ViewSeriesButton": "View series", + "CalendarDragDropMoveNotAllowedMessage": "Only events you own and can edit can be moved.", + "CalendarDragDropMoveNotAllowedTitle": "This event can't be moved", "CalendarItemAllDay": "all day", "CategoriesFolderNameOverride": "Categories", "Center": "Center", diff --git a/Wino.Core.Tests/CalendarPageViewModelTests.cs b/Wino.Core.Tests/CalendarPageViewModelTests.cs index 43c11622..63ff4faa 100644 --- a/Wino.Core.Tests/CalendarPageViewModelTests.cs +++ b/Wino.Core.Tests/CalendarPageViewModelTests.cs @@ -287,6 +287,30 @@ public class CalendarPageViewModelTests } } + [Fact] + public void CanChangeStartAndEndDate_ReturnsTrueForOrganizerMatchingAssignedCalendarAccount() + { + var account = CreateAccount(); + var calendar = CreateCalendar(account, "Calendar"); + var accountCalendarViewModel = new AccountCalendarViewModel(account, calendar); + var calendarItem = CreateCalendarItem(calendar.Id, new DateTime(2026, 3, 20, 9, 0, 0), "Existing"); + + calendarItem.AssignedCalendar = accountCalendarViewModel; + calendarItem.OrganizerEmail = account.Address; + + calendarItem.CanChangeStartAndEndDate.Should().BeTrue(); + } + + [Fact] + public void AccountCalendarViewModel_MailAccount_ExposesUnderlyingAccount() + { + var account = CreateAccount(); + var calendar = CreateCalendar(account, "Calendar"); + var accountCalendarViewModel = new AccountCalendarViewModel(account, calendar); + + accountCalendarViewModel.MailAccount.Should().BeSameAs(account); + } + private static CalendarPageViewModel CreateViewModel( ICalendarService calendarService, IPreferencesService preferencesService, diff --git a/Wino.Core.Tests/Synchronizers/WinoSynchronizerCalendarRequestTests.cs b/Wino.Core.Tests/Synchronizers/WinoSynchronizerCalendarRequestTests.cs index 333fbad3..b4ada1d5 100644 --- a/Wino.Core.Tests/Synchronizers/WinoSynchronizerCalendarRequestTests.cs +++ b/Wino.Core.Tests/Synchronizers/WinoSynchronizerCalendarRequestTests.cs @@ -79,6 +79,27 @@ public sealed class WinoSynchronizerCalendarRequestTests } } + [Fact] + public async Task Change_start_and_end_date_request_should_dispatch_to_matching_handler() + { + var synchronizer = new TestCalendarSynchronizer(throwDuringRequestExecution: false); + var calendarItemId = Guid.NewGuid(); + var request = new ChangeStartAndEndDateRequest( + new CalendarItem { Id = calendarItemId }, + []); + + synchronizer.QueueRequest(request); + + var result = await synchronizer.SynchronizeCalendarEventsAsync(new CalendarSynchronizationOptions + { + AccountId = synchronizer.Account.Id, + Type = CalendarSynchronizationType.ExecuteRequests + }); + + result.CompletedState.Should().Be(SynchronizationCompletedState.Success); + synchronizer.ChangeStartAndEndDateInvocationCount.Should().Be(1); + } + public sealed class SynchronizationActionsCompletedRecipient : IRecipient { public List CompletedAccountIds { get; } = []; @@ -98,6 +119,7 @@ public sealed class WinoSynchronizerCalendarRequestTests public override uint BatchModificationSize => 1; public override uint InitialMessageDownloadCountPerFolder => 0; + public int ChangeStartAndEndDateInvocationCount { get; private set; } public override Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default) => _throwDuringRequestExecution @@ -107,6 +129,12 @@ public sealed class WinoSynchronizerCalendarRequestTests public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) => [new TestRequestBundle(new object(), request)]; + public override List> ChangeStartAndEndDate(ChangeStartAndEndDateRequest request) + { + ChangeStartAndEndDateInvocationCount++; + return [new TestRequestBundle(new object(), request)]; + } + public override Task> CreateNewMailPackagesAsync( object message, Wino.Core.Domain.Entities.Mail.MailItemFolder assignedFolder, diff --git a/Wino.Core/Requests/Calendar/ChangeStartAndEndDateRequest.cs b/Wino.Core/Requests/Calendar/ChangeStartAndEndDateRequest.cs new file mode 100644 index 00000000..2c004ec0 --- /dev/null +++ b/Wino.Core/Requests/Calendar/ChangeStartAndEndDateRequest.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Requests.Calendar; + +/// +/// Request to move an existing calendar event by changing its start and end dates. +/// The item should already be updated in the local database before this request is queued. +/// +public record ChangeStartAndEndDateRequest(CalendarItem Item, List Attendees) + : UpdateCalendarEventRequest(Item, Attendees) +{ + public override CalendarSynchronizerOperation Operation => CalendarSynchronizerOperation.ChangeStartAndEndDate; +} diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index 7c04ef37..b98c7ec6 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -177,6 +177,11 @@ public class WinoRequestDelegator : IWinoRequestDelegator OriginalItem = calendarPreparationRequest.OriginalItem, OriginalAttendees = calendarPreparationRequest.OriginalAttendees }, + CalendarSynchronizerOperation.ChangeStartAndEndDate => new ChangeStartAndEndDateRequest(calendarPreparationRequest.CalendarItem, calendarPreparationRequest.Attendees) + { + OriginalItem = calendarPreparationRequest.OriginalItem, + OriginalAttendees = calendarPreparationRequest.OriginalAttendees + }, _ => throw new NotImplementedException($"Calendar operation {calendarPreparationRequest.Operation} is not implemented yet.") }; diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 67950529..c3f66638 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -2807,6 +2807,9 @@ public class GmailSynchronizer : WinoSynchronizer(updateRequest, request)]; } + public override List> ChangeStartAndEndDate(ChangeStartAndEndDateRequest request) + => UpdateCalendarEvent(request); + public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) { var calendarItem = request.Item; diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index befc9bbf..4556d4ad 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -334,6 +334,15 @@ public class ImapSynchronizer : WinoSynchronizer> ChangeStartAndEndDate(ChangeStartAndEndDateRequest request) + { + var handler = ResolveCalendarOperationHandler(); + return CreateCalendarOperationTaskBundle( + request, + async value => await handler.UpdateCalendarEventAsync(value).ConfigureAwait(false), + handler.RequiresConnectedClient); + } + public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) { var handler = ResolveCalendarOperationHandler(); diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 45629abf..162249a9 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -2856,6 +2856,9 @@ public class OutlookSynchronizer : WinoSynchronizer(updateRequest, request)]; } + public override List> ChangeStartAndEndDate(ChangeStartAndEndDateRequest request) + => UpdateCalendarEvent(request); + public override List> DeleteCalendarEvent(DeleteCalendarEventRequest request) { var calendarItem = request.Item; diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs index d86d6619..eab44b43 100644 --- a/Wino.Core/Synchronizers/WinoSynchronizer.cs +++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs @@ -437,6 +437,11 @@ public abstract class WinoSynchronizer() .SelectMany(UpdateCalendarEvent)); break; + case CalendarSynchronizerOperation.ChangeStartAndEndDate: + nativeRequests.AddRange(group + .OfType() + .SelectMany(ChangeStartAndEndDate)); + break; case CalendarSynchronizerOperation.DeleteEvent: nativeRequests.AddRange(group .OfType() @@ -600,6 +605,7 @@ public abstract class WinoSynchronizer> CreateCalendarEvent(CreateCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> UpdateCalendarEvent(UpdateCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); + public virtual List> ChangeStartAndEndDate(ChangeStartAndEndDateRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> DeleteCalendarEvent(DeleteCalendarEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> AcceptEvent(AcceptEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); public virtual List> DeclineEvent(DeclineEventRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType())); diff --git a/Wino.Mail.WinUI/AppThemes/Acrylic.xaml b/Wino.Mail.WinUI/AppThemes/Acrylic.xaml index 0d26ea38..b813f7de 100644 --- a/Wino.Mail.WinUI/AppThemes/Acrylic.xaml +++ b/Wino.Mail.WinUI/AppThemes/Acrylic.xaml @@ -14,6 +14,7 @@ #ecf0f1 #B2FCFCFC + #260078D4 #D9ECEFF1 #4D0078D4 @@ -27,6 +28,7 @@ #2C2C2C #662C2C2C + #33399BFF #992C2C2C #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Clouds.xaml b/Wino.Mail.WinUI/AppThemes/Clouds.xaml index 7bd2507f..671c4b17 100644 --- a/Wino.Mail.WinUI/AppThemes/Clouds.xaml +++ b/Wino.Mail.WinUI/AppThemes/Clouds.xaml @@ -13,6 +13,7 @@ #b2dffc #33B2DFFC + #59B2DFFC #66B2DFFC #4D0078D4 @@ -21,6 +22,7 @@ #b2dffc #33B2DFFC + #59B2DFFC #66B2DFFC #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Custom.xaml b/Wino.Mail.WinUI/AppThemes/Custom.xaml index fc56711b..eb13fb8c 100644 --- a/Wino.Mail.WinUI/AppThemes/Custom.xaml +++ b/Wino.Mail.WinUI/AppThemes/Custom.xaml @@ -23,6 +23,7 @@ #D9FFFFFF + #4D0078D4 @@ -37,6 +38,7 @@ #E61F1F1F + #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Default.xaml b/Wino.Mail.WinUI/AppThemes/Default.xaml index 0a1c448e..3592506a 100644 --- a/Wino.Mail.WinUI/AppThemes/Default.xaml +++ b/Wino.Mail.WinUI/AppThemes/Default.xaml @@ -14,12 +14,14 @@ #ecf0f1 #F7F9FA + #260078D4 #DFE4EA #4D0078D4 #1f1f1f #1F1F1F + #33399BFF #262626 #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Forest.xaml b/Wino.Mail.WinUI/AppThemes/Forest.xaml index 83288e03..7194ed69 100644 --- a/Wino.Mail.WinUI/AppThemes/Forest.xaml +++ b/Wino.Mail.WinUI/AppThemes/Forest.xaml @@ -13,12 +13,14 @@ #A800D608 #2200D608 + #4D00D608 #4D00D608 #4D0078D4 #59001C01 #22001C01 + #6600D608 #59001C01 #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Garden.xaml b/Wino.Mail.WinUI/AppThemes/Garden.xaml index 85b3adbe..dbf1d94f 100644 --- a/Wino.Mail.WinUI/AppThemes/Garden.xaml +++ b/Wino.Mail.WinUI/AppThemes/Garden.xaml @@ -13,6 +13,7 @@ #dcfad8 #26DCFAD8 + #59DCFAD8 #59DCFAD8 #4D0078D4 #576574 @@ -22,6 +23,7 @@ #dcfad8 #26576574 + #59576574 #59576574 #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Nighty.xaml b/Wino.Mail.WinUI/AppThemes/Nighty.xaml index 5bfcfaa3..88019e2a 100644 --- a/Wino.Mail.WinUI/AppThemes/Nighty.xaml +++ b/Wino.Mail.WinUI/AppThemes/Nighty.xaml @@ -14,6 +14,7 @@ #fdcb6e #33FDCB6E + #59FDCB6E #66FDCB6E #4D0078D4 @@ -21,6 +22,7 @@ #5413191F #2213191F + #4D13191F #5413191F #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Snowflake.xaml b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml index b768b91e..6c93b2d7 100644 --- a/Wino.Mail.WinUI/AppThemes/Snowflake.xaml +++ b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml @@ -14,6 +14,7 @@ #b0c6dd #33B0C6DD + #59B0C6DD #66B0C6DD #4D0078D4 @@ -21,6 +22,7 @@ #b0c6dd #33B0C6DD + #59B0C6DD #66B0C6DD #66399BFF diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarDragPackage.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarDragPackage.cs new file mode 100644 index 00000000..c3dacbe3 --- /dev/null +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarDragPackage.cs @@ -0,0 +1,13 @@ +using Wino.Calendar.ViewModels.Data; + +namespace Wino.Calendar.Controls; + +internal sealed class CalendarDragPackage +{ + public CalendarDragPackage(CalendarItemViewModel calendarItemViewModel) + { + CalendarItemViewModel = calendarItemViewModel; + } + + public CalendarItemViewModel CalendarItemViewModel { get; } +} diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs index 09a12209..0a3a217d 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemControl.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; +using Windows.ApplicationModel.DataTransfer; using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Messages; using Wino.Core.Domain; @@ -56,6 +57,8 @@ public sealed partial class CalendarItemControl : UserControl private void UpdateVisualStates() { + CanDrag = CalendarItem?.CanDragDrop == true; + if (CalendarItem == null) return; if (CalendarItem.IsAllDayEvent) @@ -80,7 +83,25 @@ public sealed partial class CalendarItemControl : UserControl } } - private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) => IsDragging = true; + private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) + { + if (CalendarItem?.CanDragDrop != true) + { + args.Cancel = true; + IsDragging = false; + return; + } + + args.AllowedOperations = DataPackageOperation.Move; + + var dragPackage = new CalendarDragPackage(CalendarItem); + + args.Data.Properties.Add(nameof(CalendarDragPackage), dragPackage); + args.Data.SetText(CalendarItem.DisplayTitle); + args.Data.Properties.Title = CalendarItem.DisplayTitle; + args.DragUI.SetContentFromDataPackage(); + IsDragging = true; + } private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false; diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarItemDroppedEventArgs.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemDroppedEventArgs.cs new file mode 100644 index 00000000..b2e63d6b --- /dev/null +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarItemDroppedEventArgs.cs @@ -0,0 +1,28 @@ +using System; +using Wino.Calendar.ViewModels.Data; + +namespace Wino.Calendar.Controls; + +public enum CalendarDropTargetKind +{ + TimedSlot, + TimedAllDay, + MonthCell +} + +public sealed class CalendarItemDroppedEventArgs : EventArgs +{ + public CalendarItemDroppedEventArgs( + CalendarItemViewModel calendarItemViewModel, + DateTime targetStart, + CalendarDropTargetKind targetKind) + { + CalendarItemViewModel = calendarItemViewModel; + TargetStart = targetStart; + TargetKind = targetKind; + } + + public CalendarItemViewModel CalendarItemViewModel { get; } + public DateTime TargetStart { get; } + public CalendarDropTargetKind TargetKind { get; } +} diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml index de381446..d4712bf6 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml @@ -103,8 +103,14 @@ x:Name="TimedAllDayHost" Grid.Row="1" Grid.Column="1" + AllowDrop="True" + DragLeave="CalendarDropTargetDragLeave" + DragOver="TimedAllDayHostDragOver" + Drop="TimedAllDayHostDrop" Height="{x:Bind TimedAllDayHeight, Mode=OneWay}" Background="{ThemeResource LayerFillColorDefaultBrush}" + PointerExited="CalendarDropTargetPointerExited" + PointerMoved="TimedAllDayHostPointerMoved" Visibility="{x:Bind HasTimedAllDayItems, Mode=OneWay}"> @@ -134,7 +140,13 @@ + AllowDrop="True" + DragLeave="CalendarDropTargetDragLeave" + DragOver="TimedViewportDragOver" + Drop="TimedViewportDrop" + Height="{x:Bind TimelineHeight, Mode=OneWay}" + PointerExited="CalendarDropTargetPointerExited" + PointerMoved="TimedViewportPointerMoved"> ? EmptySlotTapped; + public event EventHandler? CalendarItemDropped; private ObservableCollection TimedHeaderTextsCollection { get; } = []; private ObservableCollection MonthHeaderTextsCollection { get; } = []; @@ -182,6 +189,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty partial void OnCalendarSettingsChanged(CalendarSettings? newValue) => RequestRefresh(); partial void OnTimedHeaderDateFormatChanged(string? newValue) => RequestRefresh(); partial void OnSelectedSlotBackgroundChanged(Brush? newValue) => InvalidateStructureCanvases(); + partial void OnHoverSlotBackgroundChanged(Brush? newValue) => InvalidateStructureCanvases(); partial void OnSelectedDateTimeChanged(DateTime? newValue) => InvalidateStructureCanvases(); partial void OnCalendarItemsChanged(IReadOnlyList? newValue) @@ -263,6 +271,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private void InvalidateStructureCanvases() { + TimedAllDayCanvas.Invalidate(); TimedStructureCanvas.Invalidate(); MonthStructureCanvas.Invalidate(); } @@ -511,6 +520,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private void TimedAllDayCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e) { using var borderPaint = CreateLinePaint(); + using var hoverFillPaint = CreateFillPaint(HoverSlotBackground ?? new SolidColorBrush(Colors.Transparent)); var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); @@ -525,6 +535,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty var height = e.Info.Height; var dayWidth = (float)(_timedLayout.DayWidth * scaleX); + var hoveredTimedAllDayRect = GetHoveredTimedAllDayRect(dayWidth, height); + if (hoveredTimedAllDayRect.HasValue && hoverFillPaint.Color.Alpha > 0) + { + canvas.DrawRect(hoveredTimedAllDayRect.Value, hoverFillPaint); + } + for (var index = 1; index < _timedLayout.VisibleDates.Count; index++) { var x = dayWidth * index; @@ -541,6 +557,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty using var defaultFillPaint = CreateFillPaint(DefaultHourBackground ?? new SolidColorBrush(Colors.Transparent)); using var workFillPaint = CreateFillPaint(WorkHourBackground ?? new SolidColorBrush(Colors.Transparent)); using var selectedFillPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent)); + using var hoverFillPaint = CreateFillPaint(HoverSlotBackground ?? new SolidColorBrush(Colors.Transparent)); var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); @@ -579,6 +596,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty } } + var hoveredTimedSlotRect = GetHoveredTimedSlotRect(dayWidth, intervalHeight, intervalCount); + if (hoveredTimedSlotRect.HasValue && hoverFillPaint.Color.Alpha > 0) + { + canvas.DrawRect(hoveredTimedSlotRect.Value, hoverFillPaint); + } + var selectedTimedSlotRect = GetSelectedTimedSlotRect(dayWidth, intervalHeight, intervalCount); if (selectedTimedSlotRect.HasValue && selectedFillPaint.Color.Alpha > 0) { @@ -609,6 +632,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty IsAntialias = true }; using var selectedPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent)); + using var hoverPaint = CreateFillPaint(HoverSlotBackground ?? new SolidColorBrush(Colors.Transparent)); var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); @@ -632,6 +656,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty canvas.DrawRect((float)cell.Bounds.X, (float)cell.Bounds.Y, (float)cell.Bounds.Width, (float)cell.Bounds.Height, todayPaint); } + var hoveredMonthCellRect = GetHoveredMonthCellRect(); + if (hoveredMonthCellRect.HasValue && hoverPaint.Color.Alpha > 0) + { + canvas.DrawRect(hoveredMonthCellRect.Value, hoverPaint); + } + var selectedMonthCellRect = GetSelectedMonthCellRect(); if (selectedMonthCellRect.HasValue && selectedPaint.Color.Alpha > 0) { @@ -814,6 +844,220 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty new Size(cell.Bounds.Width, cell.Bounds.Height))); } + private void TimedViewportPointerMoved(object sender, PointerRoutedEventArgs e) + => SetHoverTarget(ResolveTimedDropTarget(e.GetCurrentPoint(TimedViewport).Position, _activeDragPackage?.CalendarItemViewModel)); + + private void TimedAllDayHostPointerMoved(object sender, PointerRoutedEventArgs e) + => SetHoverTarget(ResolveTimedAllDayDropTarget(e.GetCurrentPoint(TimedAllDayHost).Position, _activeDragPackage?.CalendarItemViewModel)); + + private void MonthViewportPointerMoved(object sender, PointerRoutedEventArgs e) + => SetHoverTarget(ResolveMonthDropTarget(e.GetCurrentPoint(MonthViewport).Position, _activeDragPackage?.CalendarItemViewModel)); + + private void CalendarDropTargetPointerExited(object sender, PointerRoutedEventArgs e) + { + if (_activeDragPackage == null) + { + SetHoverTarget(null); + } + } + + private void TimedViewportDragOver(object sender, DragEventArgs e) + { + if (!TryGetDragPackage(e, out var dragPackage)) + { + return; + } + + var hoverTarget = ResolveTimedDropTarget(e.GetPosition(TimedViewport), dragPackage.CalendarItemViewModel); + + UpdateDragOverState(e, dragPackage, hoverTarget); + } + + private void TimedAllDayHostDragOver(object sender, DragEventArgs e) + { + if (!TryGetDragPackage(e, out var dragPackage)) + { + return; + } + + var hoverTarget = ResolveTimedAllDayDropTarget(e.GetPosition(TimedAllDayHost), dragPackage.CalendarItemViewModel); + + UpdateDragOverState(e, dragPackage, hoverTarget); + } + + private void MonthViewportDragOver(object sender, DragEventArgs e) + { + if (!TryGetDragPackage(e, out var dragPackage)) + { + return; + } + + var hoverTarget = ResolveMonthDropTarget(e.GetPosition(MonthViewport), dragPackage.CalendarItemViewModel); + + UpdateDragOverState(e, dragPackage, hoverTarget); + } + + private void TimedViewportDrop(object sender, DragEventArgs e) + => HandleDrop(e, ResolveTimedDropTarget(e.GetPosition(TimedViewport), _activeDragPackage?.CalendarItemViewModel)); + + private void TimedAllDayHostDrop(object sender, DragEventArgs e) + => HandleDrop(e, ResolveTimedAllDayDropTarget(e.GetPosition(TimedAllDayHost), _activeDragPackage?.CalendarItemViewModel)); + + private void MonthViewportDrop(object sender, DragEventArgs e) + => HandleDrop(e, ResolveMonthDropTarget(e.GetPosition(MonthViewport), _activeDragPackage?.CalendarItemViewModel)); + + private void CalendarDropTargetDragLeave(object sender, DragEventArgs e) + { + _activeDragPackage = null; + SetHoverTarget(null); + } + + private bool TryGetDragPackage(DragEventArgs e, out CalendarDragPackage dragPackage) + { + dragPackage = null; + + if (!e.DataView.Properties.ContainsKey(nameof(CalendarDragPackage))) + { + e.AcceptedOperation = DataPackageOperation.None; + _activeDragPackage = null; + SetHoverTarget(null); + return false; + } + + dragPackage = e.DataView.Properties[nameof(CalendarDragPackage)] as CalendarDragPackage; + + if (dragPackage?.CalendarItemViewModel?.CanDragDrop != true) + { + e.AcceptedOperation = DataPackageOperation.None; + _activeDragPackage = null; + SetHoverTarget(null); + return false; + } + + return true; + } + + private void UpdateDragOverState(DragEventArgs e, CalendarDragPackage dragPackage, CalendarDropTargetInfo? hoverTarget) + { + _activeDragPackage = dragPackage; + SetHoverTarget(hoverTarget); + + if (hoverTarget.HasValue) + { + e.AcceptedOperation = DataPackageOperation.Move; + } + else + { + e.AcceptedOperation = DataPackageOperation.None; + } + } + + private void HandleDrop(DragEventArgs e, CalendarDropTargetInfo? hoverTarget) + { + try + { + if (_activeDragPackage?.CalendarItemViewModel?.CanDragDrop != true || !hoverTarget.HasValue) + { + e.AcceptedOperation = DataPackageOperation.None; + return; + } + + e.AcceptedOperation = DataPackageOperation.Move; + CalendarItemDropped?.Invoke( + this, + new CalendarItemDroppedEventArgs( + _activeDragPackage.CalendarItemViewModel, + hoverTarget.Value.TargetStart, + hoverTarget.Value.Kind)); + } + finally + { + _activeDragPackage = null; + SetHoverTarget(null); + } + } + + private CalendarDropTargetInfo? ResolveTimedDropTarget(Point position, CalendarItemViewModel? draggedItem) + { + if (draggedItem?.IsAllDayEvent == true || + _timedLayout.VisibleDates.Count == 0 || + _timedLayout.DayWidth <= 0) + { + return null; + } + + var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1); + var intervalHeight = GetTimedSelectionIntervalHeight(); + var slotIndex = Math.Clamp((int)(position.Y / intervalHeight), 0, (int)((24d * 60d / TimedSelectionIntervalMinutes) - 1)); + var date = _timedLayout.VisibleDates[dayIndex]; + var slotStart = TimeSpan.FromMinutes(slotIndex * TimedSelectionIntervalMinutes); + + return new CalendarDropTargetInfo( + CalendarDropTargetKind.TimedSlot, + date, + dayIndex, + slotIndex, + date.ToDateTime(TimeOnly.MinValue).Add(slotStart)); + } + + private CalendarDropTargetInfo? ResolveTimedAllDayDropTarget(Point position, CalendarItemViewModel? draggedItem) + { + if (draggedItem is { IsAllDayEvent: false } || + _timedLayout.VisibleDates.Count == 0 || + _timedLayout.DayWidth <= 0 || + TimedAllDayHeight <= 0) + { + return null; + } + + var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1); + var date = _timedLayout.VisibleDates[dayIndex]; + + return new CalendarDropTargetInfo( + CalendarDropTargetKind.TimedAllDay, + date, + dayIndex, + -1, + date.ToDateTime(TimeOnly.MinValue)); + } + + private CalendarDropTargetInfo? ResolveMonthDropTarget(Point position, CalendarItemViewModel? draggedItem) + { + if (_monthLayout.Cells.Count == 0 || _monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0) + { + return null; + } + + var column = Math.Clamp((int)(position.X / _monthLayout.CellWidth), 0, MonthCalendarLayoutCalculator.ColumnCount - 1); + var row = Math.Clamp((int)(position.Y / _monthLayout.CellHeight), 0, MonthCalendarLayoutCalculator.RowCount - 1); + var cellIndex = Math.Clamp((row * MonthCalendarLayoutCalculator.ColumnCount) + column, 0, _monthLayout.Cells.Count - 1); + var cell = _monthLayout.Cells[cellIndex]; + var targetStart = cell.Date.ToDateTime(TimeOnly.MinValue); + + if (draggedItem is { IsAllDayEvent: false }) + { + targetStart = targetStart.Add(draggedItem.StartDate.TimeOfDay); + } + + return new CalendarDropTargetInfo( + CalendarDropTargetKind.MonthCell, + cell.Date, + -1, + -1, + targetStart); + } + + private void SetHoverTarget(CalendarDropTargetInfo? hoverTarget) + { + if (_hoverTarget == hoverTarget) + { + return; + } + + _hoverTarget = hoverTarget; + InvalidateStructureCanvases(); + } + private SKRect? GetSelectedTimedSlotRect(float dayWidth, float intervalHeight, int intervalCount) { if (SelectedDateTime is not DateTime selectedDateTime || _timedLayout.VisibleDates.Count == 0) @@ -835,6 +1079,30 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty return new SKRect(x, y, x + dayWidth, y + intervalHeight); } + private SKRect? GetHoveredTimedSlotRect(float dayWidth, float intervalHeight, int intervalCount) + { + if (_hoverTarget is not { Kind: CalendarDropTargetKind.TimedSlot } hoverTarget) + { + return null; + } + + var slotIndex = Math.Clamp(hoverTarget.SlotIndex, 0, intervalCount - 1); + var x = hoverTarget.DayIndex * dayWidth; + var y = slotIndex * intervalHeight; + return new SKRect(x, y, x + dayWidth, y + intervalHeight); + } + + private SKRect? GetHoveredTimedAllDayRect(float dayWidth, float height) + { + if (_hoverTarget is not { Kind: CalendarDropTargetKind.TimedAllDay } hoverTarget) + { + return null; + } + + var x = hoverTarget.DayIndex * dayWidth; + return new SKRect(x, 0, x + dayWidth, height); + } + private SKRect? GetSelectedMonthCellRect() { if (SelectedDateTime is not DateTime selectedDateTime) @@ -860,6 +1128,30 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty return null; } + private SKRect? GetHoveredMonthCellRect() + { + if (_hoverTarget is not { Kind: CalendarDropTargetKind.MonthCell } hoverTarget) + { + return null; + } + + foreach (var cell in _monthLayout.Cells) + { + if (cell.Date != hoverTarget.Date) + { + continue; + } + + return new SKRect( + (float)cell.Bounds.X, + (float)cell.Bounds.Y, + (float)(cell.Bounds.X + cell.Bounds.Width), + (float)(cell.Bounds.Y + cell.Bounds.Height)); + } + + return null; + } + private int FindVisibleDateIndex(DateOnly date) { for (var index = 0; index < _timedLayout.VisibleDates.Count; index++) @@ -1229,6 +1521,13 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private readonly record struct CalendarTransitionInfo(CalendarTransitionKind Kind, int Direction); + private readonly record struct CalendarDropTargetInfo( + CalendarDropTargetKind Kind, + DateOnly Date, + int DayIndex, + int SlotIndex, + DateTime TargetStart); + private enum CalendarTransitionKind { None, diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml index cbafe416..e37b55ba 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml @@ -41,8 +41,10 @@ x:Name="CalendarSurface" CalendarItems="{x:Bind ViewModel.CalendarItems, Mode=OneWay}" CalendarSettings="{x:Bind ViewModel.CurrentSettings, Mode=OneWay}" + CalendarItemDropped="CalendarSurfaceCalendarItemDropped" DefaultHourBackground="{ThemeResource CalendarDefaultHourBackgroundBrush}" EmptySlotTapped="CalendarSurfaceEmptySlotTapped" + HoverSlotBackground="{ThemeResource CalendarHoverHourBackgroundBrush}" IsEnabled="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}" SelectedDateTime="{x:Bind ViewModel.SelectedQuickEventDate, Mode=OneWay}" SelectedSlotBackground="{ThemeResource CalendarSelectedHourBackgroundBrush}" diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs index 1a4c3787..16c6ba10 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml.cs @@ -173,6 +173,9 @@ public sealed partial class CalendarPage : CalendarPageAbstract, ITitleBarSearch _suppressSelectionResetOnPopupClose = false; } + private async void CalendarSurfaceCalendarItemDropped(object sender, CalendarItemDroppedEventArgs e) + => await ViewModel.MoveCalendarItemAsync(e.CalendarItemViewModel, e.TargetStart); + private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e) => QuickEventAccountSelectorFlyout.Hide();