From 125c277c889c8502c9583b6d2bd1d5e154a7415b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Mon, 6 Jan 2025 02:15:21 +0100 Subject: [PATCH] Outlook calendar/event syncing basics without delta. Bunch of UI updates for the calendar view. --- Wino.Calendar.ViewModels/AppShellViewModel.cs | 2 +- .../CalendarPageViewModel.cs | 191 +++++++++++------- .../CalendarSettingsPageViewModel.cs | 5 - .../Data/CalendarItemViewModel.cs | 8 +- .../IAccountCalendarStateService.cs | 3 - .../Messages/CalendarItemTappedMessage.cs | 5 +- .../Controls/AllDayItemsControl.xaml | 93 --------- .../Controls/AllDayItemsControl.xaml.cs | 27 --- .../Controls/CalendarItemControl.xaml | 35 +++- .../Controls/CalendarItemControl.xaml.cs | 26 ++- .../Controls/CustomCalendarFlipView.cs | 3 +- Wino.Calendar/Controls/DayColumnControl.cs | 6 +- Wino.Calendar/Controls/WinoCalendarControl.cs | 66 ++++++ .../Controls/WinoCalendarFlipView.cs | 77 +++++-- Wino.Calendar/Controls/WinoCalendarPanel.cs | 13 +- .../Controls/WinoDayTimelineCanvas.cs | 2 +- Wino.Calendar/Helpers/CalendarXamlHelpers.cs | 37 ++++ Wino.Calendar/Package.appxmanifest | 2 +- .../Services/AccountCalendarStateService.cs | 12 -- .../Styles/WinoCalendarResources.xaml | 39 +++- Wino.Calendar/Views/AppShell.xaml | 8 +- Wino.Calendar/Views/CalendarPage.xaml | 106 ++++++++++ Wino.Calendar/Views/CalendarPage.xaml.cs | 52 ++++- .../Views/Settings/CalendarSettingsPage.xaml | 11 +- Wino.Calendar/Wino.Calendar.csproj | 7 - .../Collections/CalendarEventCollection.cs | 20 +- .../Entities/Calendar/AccountCalendar.cs | 4 + .../Entities/Calendar/CalendarItem.cs | 32 ++- Wino.Core.Domain/Enums/CalendarItemStatus.cs | 1 + Wino.Core.Domain/Interfaces/ICalendarItem.cs | 3 + .../Interfaces/ICalendarItemViewModel.cs | 5 +- .../Interfaces/IPreferencesService.cs | 1 - .../Models/Calendar/CalendarSettings.cs | 3 +- .../Models/Calendar/DayRangeRenderModel.cs | 9 +- .../Translations/en_US/resources.json | 4 + Wino.Core.UWP/Helpers/XamlHelpers.cs | 2 + Wino.Core.UWP/Services/PreferencesService.cs | 9 +- .../Extensions/GoogleIntegratorExtensions.cs | 20 +- .../Extensions/OutlookIntegratorExtensions.cs | 148 ++++++++++++++ .../Processors/DefaultChangeProcessor.cs | 1 + .../Processors/GmailChangeProcessor.cs | 40 +++- .../Processors/OutlookChangeProcessor.cs | 106 ++++++++++ ...mFlatColorGenerator.cs => ColorHelpers.cs} | 12 +- .../Synchronizers/OutlookSynchronizer.cs | 173 +++++++++++++++- .../Client/Calendar/ScrollToHourMessage.cs | 10 + Wino.Services/CalendarService.cs | 21 +- 46 files changed, 1104 insertions(+), 356 deletions(-) delete mode 100644 Wino.Calendar/Controls/AllDayItemsControl.xaml delete mode 100644 Wino.Calendar/Controls/AllDayItemsControl.xaml.cs rename Wino.Core/Misc/{RandomFlatColorGenerator.cs => ColorHelpers.cs} (78%) create mode 100644 Wino.Messages/Client/Calendar/ScrollToHourMessage.cs diff --git a/Wino.Calendar.ViewModels/AppShellViewModel.cs b/Wino.Calendar.ViewModels/AppShellViewModel.cs index 3a50ffd9..55bf8c62 100644 --- a/Wino.Calendar.ViewModels/AppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/AppShellViewModel.cs @@ -224,7 +224,7 @@ namespace Wino.Calendar.ViewModels { var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions() { - AccountId = Guid.Parse("52fae547-0740-4aa3-8d51-519bd31278ca"), + AccountId = Guid.Parse("bd0fc1ab-168a-436d-86ce-0661c0eabaf9"), Type = CalendarSynchronizationType.CalendarMetadata }, SynchronizationSource.Client); diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 0fdb0982..b4235cef 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -79,8 +79,8 @@ namespace Wino.Calendar.ViewModels [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] private string _eventName; - public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(_currentSettings.GetTimeSpan(SelectedStartTimeString).Value); - public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(_currentSettings.GetTimeSpan(SelectedEndTimeString).Value); + public DateTime QuickEventStartTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedStartTimeString).Value); + public DateTime QuickEventEndTime => SelectedQuickEventDate.Value.Date.Add(CurrentSettings.GetTimeSpan(SelectedEndTimeString).Value); public bool CanSaveQuickEvent => SelectedQuickEventAccountCalendar != null && !string.IsNullOrWhiteSpace(EventName) && @@ -90,6 +90,8 @@ namespace Wino.Calendar.ViewModels #endregion + #region Data Initialization + [ObservableProperty] private DayRangeCollection _dayRanges = []; @@ -102,6 +104,20 @@ namespace Wino.Calendar.ViewModels [ObservableProperty] private bool _isCalendarEnabled = true; + #endregion + + #region Event Details + + public event EventHandler DetailsShowCalendarItemChanged; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsEventDetailsVisible))] + private CalendarItemViewModel _displayDetailsCalendarItemViewModel; + + public bool IsEventDetailsVisible => DisplayDetailsCalendarItemViewModel != null; + + #endregion + // TODO: Get rid of some of the items if we have too many. private const int maxDayRangeSize = 10; @@ -115,7 +131,9 @@ namespace Wino.Calendar.ViewModels private SemaphoreSlim _calendarLoadingSemaphore = new(1); private bool isLoadMoreBlocked = false; - private CalendarSettings _currentSettings = null; + + [ObservableProperty] + private CalendarSettings _currentSettings; public IStatePersistanceService StatePersistanceService { get; } public IAccountCalendarStateService AccountCalendarStateService { get; } @@ -148,6 +166,8 @@ namespace Wino.Calendar.ViewModels var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays); days.ForEach(a => a.EventsCollection.FilterByCalendars(AccountCalendarStateService.ActiveCalendars.Select(a => a.Id))); + + DisplayDetailsCalendarItemViewModel = null; } // TODO: Replace when calendar settings are updated. @@ -156,8 +176,8 @@ namespace Wino.Calendar.ViewModels { return displayType switch { - CalendarDisplayType.Day => new DayCalendarDrawingStrategy(_currentSettings), - CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(_currentSettings), + CalendarDisplayType.Day => new DayCalendarDrawingStrategy(CurrentSettings), + CalendarDisplayType.Week => new WeekCalendarDrawingStrategy(CurrentSettings), _ => throw new NotImplementedException(), }; } @@ -178,6 +198,26 @@ namespace Wino.Calendar.ViewModels SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary); } + [RelayCommand] + private void NavigateSeries() + { + + } + + [RelayCommand] + private void NavigateEventDetails() + { + if (DisplayDetailsCalendarItemViewModel == null) return; + + NavigateEvent(DisplayDetailsCalendarItemViewModel); + } + + [RelayCommand] + private void NavigateEvent(CalendarItemViewModel calendarItemViewModel) + { + // Double tap or clicked 'view details' of the event detail popup. + } + [RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))] private async Task SaveQuickEventAsync() { @@ -211,13 +251,33 @@ namespace Wino.Calendar.ViewModels { IsAllDay = false; - SelectedStartTimeString = _currentSettings.GetTimeString(startTime); - SelectedEndTimeString = _currentSettings.GetTimeString(endTime); + SelectedStartTimeString = CurrentSettings.GetTimeString(startTime); + SelectedEndTimeString = CurrentSettings.GetTimeString(endTime); } + // Manage event detail popup context and select-unselect the proper items. + // Item selection rules are defined in the selection method. + partial void OnDisplayDetailsCalendarItemViewModelChanging(CalendarItemViewModel oldValue, CalendarItemViewModel newValue) + { + if (oldValue != null) + { + UnselectCalendarItem(oldValue); + } + + if (newValue != null) + { + SelectCalendarItem(newValue); + } + } + + // Notify view that the detail context changed. + // This will align the event detail popup to the selected event. + partial void OnDisplayDetailsCalendarItemViewModelChanged(CalendarItemViewModel value) + => DetailsShowCalendarItemChanged?.Invoke(this, EventArgs.Empty); + private void RefreshSettings() { - _currentSettings = _preferencesService.GetCurrentCalendarSettings(); + CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); // Populate the hour selection strings. var timeStrings = new List(); @@ -228,7 +288,7 @@ namespace Wino.Calendar.ViewModels { var time = new DateTime(1, 1, 1, hour, minute, 0); - if (_currentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour) + if (CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour) { timeStrings.Add(time.ToString("HH:mm")); } @@ -255,7 +315,6 @@ namespace Wino.Calendar.ViewModels // 2. Day display count is different. // 3. Display date is not in the visible range. - if (DayRanges.DisplayRange == null) return false; return @@ -289,6 +348,9 @@ namespace Wino.Calendar.ViewModels await RenderDatesAsync(message.CalendarInitInitiative, message.DisplayDate, CalendarLoadDirection.Replace); + + // Scroll to the current hour. + Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour))); } catch (Exception ex) { @@ -403,7 +465,7 @@ namespace Wino.Calendar.ViewModels var endDate = startDate.AddDays(eachFlipItemCount); var range = new DateRange(startDate, endDate); - var renderOptions = new CalendarRenderOptions(range, _currentSettings); + var renderOptions = new CalendarRenderOptions(range, CurrentSettings); var dayRangeHeaderModel = new DayRangeRenderModel(renderOptions); renderModels.Add(dayRangeHeaderModel); @@ -613,14 +675,9 @@ namespace Wino.Calendar.ViewModels } } - partial void OnSelectedQuickEventDateChanged(DateTime? value) - { - - } - partial void OnSelectedStartTimeStringChanged(string newValue) { - var parsedTime = _currentSettings.GetTimeSpan(newValue); + var parsedTime = CurrentSettings.GetTimeSpan(newValue); if (parsedTime == null) { @@ -634,7 +691,7 @@ namespace Wino.Calendar.ViewModels partial void OnSelectedEndTimeStringChanged(string newValue) { - var parsedTime = _currentSettings.GetTimeSpan(newValue); + var parsedTime = CurrentSettings.GetTimeSpan(newValue); if (parsedTime == null) { @@ -648,6 +705,8 @@ namespace Wino.Calendar.ViewModels partial void OnSelectedDayRangeChanged(DayRangeRenderModel value) { + DisplayDetailsCalendarItemViewModel = null; + if (DayRanges.Count == 0 || SelectedDateRangeIndex < 0) return; var selectedRange = DayRanges[SelectedDateRangeIndex]; @@ -699,71 +758,63 @@ namespace Wino.Calendar.ViewModels // Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true)); } - private IEnumerable GetCalendarItems(Guid calendarItemId) + private IEnumerable GetCalendarItems(CalendarItemViewModel calendarItemViewModel, CalendarDayModel selectedDay) { - // Multi-day events are sprated in multiple days. + // 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. - return DayRanges - .SelectMany(a => a.CalendarDays) - .Select(b => b.EventsCollection.GetCalendarItem(calendarItemId)) - .Where(c => c != null) - .Cast() - .Distinct(); - } - - private void ResetSelectedItems() - { - foreach (var item in AccountCalendarStateService.SelectedItems) + if (calendarItemViewModel.IsSingleExceptionalInstance) { - var items = GetCalendarItems(item.Id); - - foreach (var childItem in items) - { - childItem.IsSelected = false; - } + return [calendarItemViewModel]; + } + else + { + return DayRanges + .SelectMany(a => a.CalendarDays) + .Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id)) + .Where(c => c != null) + .Cast() + .Distinct(); } - - AccountCalendarStateService.SelectedItems.Clear(); } - public async void Receive(CalendarItemTappedMessage message) + private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null) + { + if (calendarItemViewModel == null) return; + + var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay); + + foreach (var item in itemsToUnselect) + { + item.IsSelected = false; + } + } + + private void SelectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null) + { + if (calendarItemViewModel == null) return; + + var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay); + + foreach (var item in itemsToSelect) + { + item.IsSelected = true; + } + } + + public void Receive(CalendarItemTappedMessage message) { if (message.CalendarItemViewModel == null) return; - await ExecuteUIThread(() => - { - var calendarItems = GetCalendarItems(message.CalendarItemViewModel.Id); - - if (!_keyPressService.IsCtrlKeyPressed()) - { - ResetSelectedItems(); - } - - foreach (var item in calendarItems) - { - item.IsSelected = !item.IsSelected; - - // Multi-select logic. - if (item.IsSelected && !AccountCalendarStateService.SelectedItems.Contains(item)) - { - AccountCalendarStateService.SelectedItems.Add(message.CalendarItemViewModel); - } - else if (!item.IsSelected && AccountCalendarStateService.SelectedItems.Contains(item)) - { - AccountCalendarStateService.SelectedItems.Remove(item); - } - } - }); + DisplayDetailsCalendarItemViewModel = message.CalendarItemViewModel; } - public void Receive(CalendarItemDoubleTappedMessage message) - { - // TODO: Navigate to the event details page. - } + public void Receive(CalendarItemDoubleTappedMessage message) => NavigateEvent(message.CalendarItemViewModel); public void Receive(CalendarItemRightTappedMessage message) { + } public async void Receive(CalendarItemDeleted message) @@ -777,9 +828,7 @@ namespace Wino.Calendar.ViewModels // Event might be spreaded into multiple days. // Remove from all. - var calendarItems = GetCalendarItems(deletedItem.Id); - - + // var calendarItems = GetCalendarItems(deletedItem.Id); }); } } diff --git a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs index 5fe0840b..b9c21f97 100644 --- a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs @@ -21,9 +21,6 @@ namespace Wino.Calendar.ViewModels [ObservableProperty] private bool _is24HourHeaders; - [ObservableProperty] - private bool _ghostRenderAllDayEvents; - [ObservableProperty] private TimeSpan _workingHourStart; @@ -64,7 +61,6 @@ namespace Wino.Calendar.ViewModels _workingHourStart = preferencesService.WorkingHourStart; _workingHourEnd = preferencesService.WorkingHourEnd; _cellHourHeight = preferencesService.HourHeight; - _ghostRenderAllDayEvents = preferencesService.GhostRenderAllDayEvents; _workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart)); _workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd)); @@ -79,7 +75,6 @@ namespace Wino.Calendar.ViewModels partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings(); partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); - partial void OnGhostRenderAllDayEventsChanged(bool value) => SaveSettings(); public void SaveSettings() { diff --git a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs index d75ece73..721d4931 100644 --- a/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/CalendarItemViewModel.cs @@ -24,11 +24,13 @@ namespace Wino.Calendar.ViewModels.Data public ITimePeriod Period => CalendarItem.Period; - public bool IsAllDayEvent => ((ICalendarItem)CalendarItem).IsAllDayEvent; + public bool IsAllDayEvent => CalendarItem.IsAllDayEvent; - public bool IsMultiDayEvent => ((ICalendarItem)CalendarItem).IsMultiDayEvent; + public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent; - public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.Recurrence); + public bool IsRecurringEvent => CalendarItem.IsRecurringEvent; + + public bool IsSingleExceptionalInstance => CalendarItem.IsSingleExceptionalInstance; [ObservableProperty] private bool _isSelected; diff --git a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs index 2865ccd0..1a11a327 100644 --- a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs +++ b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs @@ -22,9 +22,6 @@ namespace Wino.Calendar.ViewModels.Interfaces public void AddAccountCalendar(AccountCalendarViewModel accountCalendar); public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar); - ObservableCollection SelectedItems { get; } - bool HasMultipleSelectedItems { get; } - /// /// Enumeration of currently selected calendars. /// diff --git a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs index edebe9ea..cd79dbe0 100644 --- a/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs +++ b/Wino.Calendar.ViewModels/Messages/CalendarItemTappedMessage.cs @@ -1,14 +1,17 @@ using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain.Models.Calendar; namespace Wino.Calendar.ViewModels.Messages { public class CalendarItemTappedMessage { - public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel) + public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod) { CalendarItemViewModel = calendarItemViewModel; + ClickedPeriod = clickedPeriod; } public CalendarItemViewModel CalendarItemViewModel { get; } + public CalendarDayModel ClickedPeriod { get; } } } diff --git a/Wino.Calendar/Controls/AllDayItemsControl.xaml b/Wino.Calendar/Controls/AllDayItemsControl.xaml deleted file mode 100644 index 3d7c7f6a..00000000 --- a/Wino.Calendar/Controls/AllDayItemsControl.xaml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wino.Calendar/Controls/AllDayItemsControl.xaml.cs b/Wino.Calendar/Controls/AllDayItemsControl.xaml.cs deleted file mode 100644 index cd4b0466..00000000 --- a/Wino.Calendar/Controls/AllDayItemsControl.xaml.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Wino.Core.Domain.Models.Calendar; - - -namespace Wino.Calendar.Controls -{ - public sealed partial class AllDayItemsControl : UserControl - { - #region Dependency Properties - - public static readonly DependencyProperty CalendarDayModelProperty = DependencyProperty.Register(nameof(CalendarDayModel), typeof(CalendarDayModel), typeof(AllDayItemsControl), new PropertyMetadata(null)); - - public CalendarDayModel CalendarDayModel - { - get { return (CalendarDayModel)GetValue(CalendarDayModelProperty); } - set { SetValue(CalendarDayModelProperty, value); } - } - - #endregion - - public AllDayItemsControl() - { - InitializeComponent(); - } - } -} diff --git a/Wino.Calendar/Controls/CalendarItemControl.xaml b/Wino.Calendar/Controls/CalendarItemControl.xaml index c87b7798..00c9054e 100644 --- a/Wino.Calendar/Controls/CalendarItemControl.xaml +++ b/Wino.Calendar/Controls/CalendarItemControl.xaml @@ -42,6 +42,7 @@ @@ -61,18 +62,19 @@ + Orientation="Horizontal" + Spacing="6"> @@ -83,7 +85,8 @@ - + + @@ -105,20 +108,27 @@ + + + + - + + - - + + + + @@ -126,11 +136,18 @@ + + + + - + diff --git a/Wino.Calendar/Controls/CalendarItemControl.xaml.cs b/Wino.Calendar/Controls/CalendarItemControl.xaml.cs index 9aca6bf0..1d20e356 100644 --- a/Wino.Calendar/Controls/CalendarItemControl.xaml.cs +++ b/Wino.Calendar/Controls/CalendarItemControl.xaml.cs @@ -1,8 +1,10 @@ using System.Diagnostics; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Itenso.TimePeriod; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Messages; using Wino.Core.Domain; @@ -12,7 +14,8 @@ namespace Wino.Calendar.Controls { public sealed partial class CalendarItemControl : UserControl { - public bool IsAllDayMultiDayEvent { get; set; } + // Single tap has a delay to report double taps properly. + private bool isSingleTap = false; public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged))); public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false)); @@ -163,21 +166,30 @@ namespace Wino.Calendar.Controls private void ControlDropped(UIElement sender, DropCompletedEventArgs args) => IsDragging = false; - private void ControlTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e) + private async void ControlTapped(object sender, TappedRoutedEventArgs e) { if (CalendarItem == null) return; - WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem)); + isSingleTap = true; + + await Task.Delay(100); + + if (isSingleTap) + { + WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate)); + } } - private void ControlDoubleTapped(object sender, Windows.UI.Xaml.Input.DoubleTappedRoutedEventArgs e) + private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e) { if (CalendarItem == null) return; + isSingleTap = false; + WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem)); } - private void ControlRightTapped(object sender, Windows.UI.Xaml.Input.RightTappedRoutedEventArgs e) + private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e) { if (CalendarItem == null) return; @@ -188,10 +200,6 @@ namespace Wino.Calendar.Controls { if (CalendarItem == null) return; - if (!CalendarItem.IsSelected) - { - WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem)); - } } } } diff --git a/Wino.Calendar/Controls/CustomCalendarFlipView.cs b/Wino.Calendar/Controls/CustomCalendarFlipView.cs index e8a5b8e4..11382393 100644 --- a/Wino.Calendar/Controls/CustomCalendarFlipView.cs +++ b/Wino.Calendar/Controls/CustomCalendarFlipView.cs @@ -24,6 +24,8 @@ namespace Wino.Calendar.Controls // Hide navigation buttons PreviousButton.Opacity = NextButton.Opacity = 0; PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false; + + var t = FindName("ScrollingHost"); } public void GoPreviousFlip() @@ -37,6 +39,5 @@ namespace Wino.Calendar.Controls var nextPeer = new ButtonAutomationPeer(NextButton); nextPeer.Invoke(); } - } } diff --git a/Wino.Calendar/Controls/DayColumnControl.cs b/Wino.Calendar/Controls/DayColumnControl.cs index 1f806f41..ea7b34ee 100644 --- a/Wino.Calendar/Controls/DayColumnControl.cs +++ b/Wino.Calendar/Controls/DayColumnControl.cs @@ -19,7 +19,7 @@ namespace Wino.Calendar.Controls private TextBlock HeaderDateDayText; private TextBlock ColumnHeaderText; private Border IsTodayBorder; - private AllDayItemsControl AllDayItemsControl; + private ItemsControl AllDayItemsControl; public CalendarDayModel DayModel { @@ -41,7 +41,7 @@ namespace Wino.Calendar.Controls HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock; ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock; IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border; - AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as AllDayItemsControl; + AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl; UpdateValues(); } @@ -61,7 +61,7 @@ namespace Wino.Calendar.Controls HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString(); ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo); - AllDayItemsControl.CalendarDayModel = DayModel; + AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date; diff --git a/Wino.Calendar/Controls/WinoCalendarControl.cs b/Wino.Calendar/Controls/WinoCalendarControl.cs index 6e471e53..868fe9ea 100644 --- a/Wino.Calendar/Controls/WinoCalendarControl.cs +++ b/Wino.Calendar/Controls/WinoCalendarControl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Wino.Calendar.Args; @@ -18,6 +19,8 @@ namespace Wino.Calendar.Controls public event EventHandler TimelineCellSelected; public event EventHandler TimelineCellUnselected; + public event EventHandler ScrollPositionChanging; + #region Dependency Properties public static readonly DependencyProperty DayRangesProperty = DependencyProperty.Register(nameof(DayRanges), typeof(ObservableCollection), typeof(WinoCalendarControl), new PropertyMetadata(null)); @@ -25,6 +28,7 @@ namespace Wino.Calendar.Controls public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null)); public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveCanvasChanged))); public static readonly DependencyProperty IsFlipIdleProperty = DependencyProperty.Register(nameof(IsFlipIdle), typeof(bool), typeof(WinoCalendarControl), new PropertyMetadata(true, new PropertyChangedCallback(OnIdleStateChanged))); + public static readonly DependencyProperty ActiveScrollViewerProperty = DependencyProperty.Register(nameof(ActiveScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnActiveVerticalScrollViewerChanged))); public DayRangeRenderModel SelectedFlipViewDayRange { @@ -32,6 +36,12 @@ namespace Wino.Calendar.Controls set { SetValue(SelectedFlipViewDayRangeProperty, value); } } + public ScrollViewer ActiveScrollViewer + { + get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); } + set { SetValue(ActiveScrollViewerProperty, value); } + } + public WinoDayTimelineCanvas ActiveCanvas { get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); } @@ -79,6 +89,22 @@ namespace Wino.Calendar.Controls } } + private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) + { + if (calendar is WinoCalendarControl calendarControl) + { + if (e.OldValue is ScrollViewer oldScrollViewer) + { + calendarControl.DeregisterScrollChanges(oldScrollViewer); + } + + if (e.NewValue is ScrollViewer newScrollViewer) + { + calendarControl.RegisterScrollChanges(newScrollViewer); + } + } + } + private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) { if (calendar is WinoCalendarControl calendarControl) @@ -128,6 +154,23 @@ namespace Wino.Calendar.Controls canvas.TimelineCellUnselected += ActiveTimelineCellUnselected; } + private void RegisterScrollChanges(ScrollViewer scrollViewer) + { + if (scrollViewer == null) return; + + scrollViewer.ViewChanging += ScrollViewChanging; + } + + private void DeregisterScrollChanges(ScrollViewer scrollViewer) + { + if (scrollViewer == null) return; + + scrollViewer.ViewChanging -= ScrollViewChanging; + } + + private void ScrollViewChanging(object sender, ScrollViewerViewChangingEventArgs e) + => ScrollPositionChanging?.Invoke(this, EventArgs.Empty); + private void CalendarSizeChanged(object sender, SizeChangedEventArgs e) { if (ActiveCanvas == null) return; @@ -159,6 +202,22 @@ namespace Wino.Calendar.Controls public void NavigateToDay(DateTime dateTime) => InternalFlipView.NavigateToDay(dateTime); + public async void NavigateToHour(TimeSpan timeSpan) + { + if (ActiveScrollViewer == null) return; + + // Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers. + + await Task.Yield(); + await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () => + { + double hourHeght = 60; + double totalHeight = ActiveScrollViewer.ScrollableHeight; + double scrollPosition = timeSpan.TotalHours * hourHeght; + + ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false); + }); + } public void ResetTimelineSelection() { if (ActiveCanvas == null) return; @@ -180,6 +239,13 @@ namespace Wino.Calendar.Controls InternalFlipView.GoPreviousFlip(); } + public void UnselectActiveTimelineCell() + { + if (ActiveCanvas == null) return; + + ActiveCanvas.SelectedDateTime = null; + } + public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel) { if (ActiveCanvas == null) return null; diff --git a/Wino.Calendar/Controls/WinoCalendarFlipView.cs b/Wino.Calendar/Controls/WinoCalendarFlipView.cs index e8c14eb8..14dea2fe 100644 --- a/Wino.Calendar/Controls/WinoCalendarFlipView.cs +++ b/Wino.Calendar/Controls/WinoCalendarFlipView.cs @@ -14,13 +14,29 @@ namespace Wino.Calendar.Controls { public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true)); public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null)); + public static readonly DependencyProperty ActiveVerticalScrollViewerProperty = DependencyProperty.Register(nameof(ActiveVerticalScrollViewer), typeof(ScrollViewer), typeof(WinoCalendarFlipView), new PropertyMetadata(null)); + /// + /// Gets or sets the active canvas that is currently displayed in the flip view. + /// Each day-range of flip view item has a canvas that displays the day timeline. + /// public WinoDayTimelineCanvas ActiveCanvas { get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); } set { SetValue(ActiveCanvasProperty, value); } } + /// + /// Gets or sets the scroll viewer that is currently active in the flip view. + /// It's the vertical scroll that scrolls the timeline only, not the header part that belongs + /// to parent FlipView control. + /// + public ScrollViewer ActiveVerticalScrollViewer + { + get { return (ScrollViewer)GetValue(ActiveVerticalScrollViewerProperty); } + set { SetValue(ActiveVerticalScrollViewerProperty, value); } + } + public bool IsIdle { get { return (bool)GetValue(IsIdleProperty); } @@ -46,6 +62,7 @@ namespace Wino.Calendar.Controls if (d is WinoCalendarFlipView flipView) { flipView.UpdateActiveCanvas(); + flipView.UpdateActiveScrollViewer(); } } @@ -62,22 +79,58 @@ namespace Wino.Calendar.Controls IsIdle = e.Action == NotifyCollectionChangedAction.Reset || e.Action == NotifyCollectionChangedAction.Replace; } - public async void UpdateActiveCanvas() + private async Task GetCurrentFlipViewItem() + { + // TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together. + while (ContainerFromIndex(SelectedIndex) == null) + { + await Task.Delay(100); + } + + return ContainerFromIndex(SelectedIndex) as FlipViewItem; + + + } + + private void UpdateActiveScrollViewer() + { + if (SelectedIndex < 0) + ActiveVerticalScrollViewer = null; + else + { + GetCurrentFlipViewItem().ContinueWith(task => + { + if (task.IsCompletedSuccessfully) + { + var flipViewItem = task.Result; + + _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + { + ActiveVerticalScrollViewer = flipViewItem.FindDescendant(); + }); + } + }); + } + } + + public void UpdateActiveCanvas() { if (SelectedIndex < 0) ActiveCanvas = null; else { - // TODO: Refactor this mechanism by listening to PrepareContainerForItemOverride and Loaded events together. - while (ContainerFromIndex(SelectedIndex) == null) + GetCurrentFlipViewItem().ContinueWith(task => { - await Task.Delay(100); - } + if (task.IsCompletedSuccessfully) + { + var flipViewItem = task.Result; - if (ContainerFromIndex(SelectedIndex) is FlipViewItem flipViewItem) - { - ActiveCanvas = flipViewItem.FindDescendant(); - } + _ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + { + ActiveCanvas = flipViewItem.FindDescendant(); + }); + } + }); } } @@ -127,12 +180,6 @@ namespace Wino.Calendar.Controls }); } - public void NavigateHour(TimeSpan hourTimeSpan) - { - // Total height of the FlipViewItem is the same as vertical ScrollViewer to position day headers. - // Find the day range that contains the hour. - } - private ObservableRangeCollection GetItemsSource() => ItemsSource as ObservableRangeCollection; } diff --git a/Wino.Calendar/Controls/WinoCalendarPanel.cs b/Wino.Calendar/Controls/WinoCalendarPanel.cs index 0adc5ae6..b4c32080 100644 --- a/Wino.Calendar/Controls/WinoCalendarPanel.cs +++ b/Wino.Calendar/Controls/WinoCalendarPanel.cs @@ -1,7 +1,6 @@  using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using CommunityToolkit.WinUI; using Itenso.TimePeriod; @@ -79,7 +78,7 @@ namespace Wino.Calendar.Controls var periodRelation = child.Period.GetRelation(Period); - Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}"); + // Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}"); if (!child.IsMultiDayEvent) { @@ -169,6 +168,15 @@ namespace Wino.Calendar.Controls if (childWidth < 0) childWidth = 1; + // Regular events must have 2px margin + if (!child.IsMultiDayEvent && !child.IsAllDayEvent) + { + childLeft += 2; + childTop += 2; + childHeight -= 2; + childWidth -= 2; + } + var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight); // Make sure measured size will fit in the arranged box. @@ -179,6 +187,7 @@ namespace Wino.Calendar.Controls //Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}"); } + return finalSize; } diff --git a/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs b/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs index 029d840d..e77088fe 100644 --- a/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs +++ b/Wino.Calendar/Controls/WinoDayTimelineCanvas.cs @@ -145,8 +145,8 @@ namespace Wino.Calendar.Controls } else { - TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize)); SelectedDateTime = clickedDateTime; + TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize)); } Debug.WriteLine($"Clicked: {clickedDateTime}"); diff --git a/Wino.Calendar/Helpers/CalendarXamlHelpers.cs b/Wino.Calendar/Helpers/CalendarXamlHelpers.cs index 8f8decbb..288b8c21 100644 --- a/Wino.Calendar/Helpers/CalendarXamlHelpers.cs +++ b/Wino.Calendar/Helpers/CalendarXamlHelpers.cs @@ -1,6 +1,11 @@ using System.Linq; +using Windows.UI.Xaml.Controls.Primitives; using Wino.Calendar.ViewModels.Data; +using Wino.Core.Domain; using Wino.Core.Domain.Collections; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Calendar; +using Wino.Helpers; namespace Wino.Calendar.Helpers { @@ -8,5 +13,37 @@ namespace Wino.Calendar.Helpers { public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection) => (CalendarItemViewModel)collection.AllDayEvents.FirstOrDefault(); + + public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings) + { + if (calendarItemViewModel == null || settings == null) return string.Empty; + + // Single event in a day. + if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent) + { + return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}"; + } + else if (calendarItemViewModel.IsMultiDayEvent) + { + return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}"; + } + else + { + // All day event. + return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})"; + } + } + + public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup( + CalendarItemViewModel calendarItemViewModel, + CalendarDisplayType calendarDisplayType) + { + if (calendarItemViewModel == null) return PopupPlacementMode.Auto; + + // All and/or multi day events always go to the top of the screen. + if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom; + + return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType); + } } } diff --git a/Wino.Calendar/Package.appxmanifest b/Wino.Calendar/Package.appxmanifest index 4385dc74..004cec01 100644 --- a/Wino.Calendar/Package.appxmanifest +++ b/Wino.Calendar/Package.appxmanifest @@ -20,7 +20,7 @@ + Version="1.0.13.0" /> diff --git a/Wino.Calendar/Services/AccountCalendarStateService.cs b/Wino.Calendar/Services/AccountCalendarStateService.cs index ea91a4b5..cf4300a3 100644 --- a/Wino.Calendar/Services/AccountCalendarStateService.cs +++ b/Wino.Calendar/Services/AccountCalendarStateService.cs @@ -18,11 +18,6 @@ namespace Wino.Calendar.Services public event EventHandler CollectiveAccountGroupSelectionStateChanged; public event EventHandler AccountCalendarSelectionStateChanged; - [ObservableProperty] - public ObservableCollection _selectedItems = new ObservableCollection(); - - public bool HasMultipleSelectedItems => SelectedItems.Count > 1; - [ObservableProperty] private ReadOnlyObservableCollection groupedAccountCalendars; @@ -52,13 +47,6 @@ namespace Wino.Calendar.Services public AccountCalendarStateService() { GroupedAccountCalendars = new ReadOnlyObservableCollection(_internalGroupedAccountCalendars); - - SelectedItems.CollectionChanged += SelectedCalendarItemsUpdated; - } - - private void SelectedCalendarItemsUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - OnPropertyChanged(nameof(HasMultipleSelectedItems)); } private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e) diff --git a/Wino.Calendar/Styles/WinoCalendarResources.xaml b/Wino.Calendar/Styles/WinoCalendarResources.xaml index 5fdd576b..c7f6f591 100644 --- a/Wino.Calendar/Styles/WinoCalendarResources.xaml +++ b/Wino.Calendar/Styles/WinoCalendarResources.xaml @@ -9,6 +9,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="using:Wino.Core.Domain.Models.Calendar" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + xmlns:selectors="using:Wino.Calendar.Selectors" xmlns:toolkitControls="using:CommunityToolkit.WinUI.Controls"> @@ -90,8 +91,6 @@ - - @@ -138,6 +137,7 @@ HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ActiveCanvas="{x:Bind ActiveCanvas, Mode=TwoWay}" + ActiveVerticalScrollViewer="{x:Bind ActiveScrollViewer, Mode=TwoWay}" Background="Transparent" IsIdle="{x:Bind IsFlipIdle, Mode=TwoWay}" IsTabStop="False" @@ -220,11 +220,42 @@ - + Margin="0,6"> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Calendar/Views/AppShell.xaml b/Wino.Calendar/Views/AppShell.xaml index 51b9bd1e..6fcc4083 100644 --- a/Wino.Calendar/Views/AppShell.xaml +++ b/Wino.Calendar/Views/AppShell.xaml @@ -150,7 +150,11 @@ Grid.RowSpan="2" Grid.ColumnSpan="2" Background="{ThemeResource WinoApplicationBackgroundColor}" - IsHitTestVisible="False" /> + IsHitTestVisible="False"> + + + + diff --git a/Wino.Calendar/Views/CalendarPage.xaml b/Wino.Calendar/Views/CalendarPage.xaml index fbc92464..30cfc716 100644 --- a/Wino.Calendar/Views/CalendarPage.xaml +++ b/Wino.Calendar/Views/CalendarPage.xaml @@ -5,6 +5,7 @@ xmlns:abstract="using:Wino.Calendar.Views.Abstract" xmlns:animations="using:CommunityToolkit.WinUI.Animations" xmlns:calendarControls="using:Wino.Calendar.Controls" + xmlns:calendarHelpers="using:Wino.Calendar.Helpers" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:data="using:Wino.Calendar.ViewModels.Data" xmlns:domain="using:Wino.Core.Domain" @@ -33,6 +34,7 @@ x:Name="CalendarControl" DayRanges="{x:Bind ViewModel.DayRanges}" IsHitTestVisible="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}" + ScrollPositionChanging="CalendarScrolling" SelectedFlipViewDayRange="{x:Bind ViewModel.SelectedDayRange, Mode=TwoWay}" SelectedFlipViewIndex="{x:Bind ViewModel.SelectedDateRangeIndex, Mode=TwoWay}" TimelineCellSelected="CellSelected" @@ -284,6 +286,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +