Range thing.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -9,16 +10,15 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Serilog;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Calendar.ViewModels.Interfaces;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Collections;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.MenuItems;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.ViewModels;
|
||||
@@ -30,8 +30,6 @@ namespace Wino.Calendar.ViewModels;
|
||||
|
||||
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
ICalendarShellClient,
|
||||
IRecipient<VisibleDateRangeChangedMessage>,
|
||||
IRecipient<CalendarEnableStatusChangedMessage>,
|
||||
IRecipient<CalendarDisplayTypeChangedMessage>,
|
||||
IRecipient<AccountRemovedMessage>
|
||||
{
|
||||
@@ -41,6 +39,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
public INavigationService NavigationService { get; }
|
||||
public WinoApplicationMode Mode => WinoApplicationMode.Calendar;
|
||||
public bool HandlesNavigationSelection => false;
|
||||
public VisibleDateRange CurrentVisibleRange => _calendarPageViewModel.CurrentVisibleRange;
|
||||
public string VisibleDateRangeText => _calendarPageViewModel.VisibleDateRangeText;
|
||||
System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars;
|
||||
System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems;
|
||||
object IShellClient.SelectedMenuItem
|
||||
@@ -50,6 +50,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
}
|
||||
System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand;
|
||||
System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
|
||||
System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand;
|
||||
System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand;
|
||||
|
||||
public MenuItemCollection MenuItems { get; private set; }
|
||||
public MenuItemCollection FooterItems { get; private set; }
|
||||
@@ -57,23 +59,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
[ObservableProperty]
|
||||
private int _selectedMenuItemIndex = -1;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isCalendarEnabled;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display date of the calendar.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DateTimeOffset _displayDate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DateRange highlightedDateRange;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
|
||||
|
||||
@@ -87,36 +72,44 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
|
||||
private readonly SettingsItem _settingsItem = new();
|
||||
private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new();
|
||||
|
||||
// For updating account calendars asynchronously.
|
||||
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||
private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||
private readonly CalendarPageViewModel _calendarPageViewModel;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IUpdateManager _updateManager;
|
||||
private readonly IStoreUpdateService _storeUpdateService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IDateContextProvider _dateContextProvider;
|
||||
private bool _runtimeSubscriptionsAttached;
|
||||
private bool _hasRegisteredPersistentRecipients;
|
||||
private DateTime? _navigationDate;
|
||||
|
||||
public CalendarAppShellViewModel(IPreferencesService preferencesService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INavigationService navigationService,
|
||||
CalendarPageViewModel calendarPageViewModel,
|
||||
IMailDialogService dialogService,
|
||||
IUpdateManager updateManager,
|
||||
IStoreUpdateService storeUpdateService)
|
||||
public CalendarAppShellViewModel(
|
||||
IPreferencesService preferencesService,
|
||||
IStatePersistanceService statePersistanceService,
|
||||
IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INavigationService navigationService,
|
||||
CalendarPageViewModel calendarPageViewModel,
|
||||
IMailDialogService dialogService,
|
||||
IUpdateManager updateManager,
|
||||
IStoreUpdateService storeUpdateService,
|
||||
IDateContextProvider dateContextProvider)
|
||||
{
|
||||
PreferencesService = preferencesService;
|
||||
StatePersistenceService = statePersistanceService;
|
||||
AccountCalendarStateService = accountCalendarStateService;
|
||||
NavigationService = navigationService;
|
||||
_accountService = accountService;
|
||||
_calendarService = calendarService;
|
||||
_calendarPageViewModel = calendarPageViewModel;
|
||||
_dialogService = dialogService;
|
||||
_updateManager = updateManager;
|
||||
_storeUpdateService = storeUpdateService;
|
||||
_dateContextProvider = dateContextProvider;
|
||||
|
||||
AccountCalendarStateService = accountCalendarStateService;
|
||||
|
||||
NavigationService = navigationService;
|
||||
PreferencesService = preferencesService;
|
||||
|
||||
StatePersistenceService = statePersistanceService;
|
||||
_calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
@@ -129,17 +122,30 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
_ = RefreshFooterItemsAsync(false);
|
||||
}
|
||||
|
||||
private void CalendarPageViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange))
|
||||
{
|
||||
OnPropertyChanged(nameof(CurrentVisibleRange));
|
||||
}
|
||||
|
||||
if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange) ||
|
||||
e.PropertyName == nameof(CalendarPageViewModel.VisibleDateRangeText))
|
||||
{
|
||||
OnPropertyChanged(nameof(VisibleDateRangeText));
|
||||
UpdateDateNavigationHeaderItems();
|
||||
}
|
||||
}
|
||||
|
||||
private void PrefefencesChanged(object sender, string e)
|
||||
{
|
||||
if (e == nameof(StatePersistenceService.CalendarDisplayType))
|
||||
{
|
||||
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
|
||||
if (e != nameof(StatePersistenceService.CalendarDisplayType))
|
||||
return;
|
||||
|
||||
UpdateDateNavigationHeaderItems();
|
||||
|
||||
// Change the calendar.
|
||||
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
|
||||
}
|
||||
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
|
||||
OnPropertyChanged(nameof(IsVerticalCalendar));
|
||||
UpdateDateNavigationHeaderItems();
|
||||
NavigateCalendarDate(GetDisplayTypeSwitchDate());
|
||||
}
|
||||
|
||||
private async void PreferencesServiceChanged(object sender, string e)
|
||||
@@ -167,9 +173,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||
|
||||
await RefreshFooterItemsAsync(mode == NavigationMode.New);
|
||||
|
||||
UpdateDateNavigationHeaderItems();
|
||||
|
||||
await InitializeAccountCalendarsAsync();
|
||||
ValidateConfiguredNewEventCalendar();
|
||||
|
||||
@@ -189,7 +193,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
{
|
||||
DateNavigationHeaderItems.Clear();
|
||||
AccountCalendarStateService.ClearGroupedAccountCalendars();
|
||||
HighlightedDateRange = null;
|
||||
SelectedDateNavigationHeaderIndex = -1;
|
||||
});
|
||||
_calendarPageViewModel.CleanupForShellDeactivation();
|
||||
@@ -223,7 +226,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
return;
|
||||
|
||||
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
||||
|
||||
if (notes.Sections.Count == 0)
|
||||
return;
|
||||
|
||||
@@ -246,10 +248,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
|
||||
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
|
||||
{
|
||||
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
|
||||
// Reporting all these changes one by one to the UI is not efficient and may cause problems in the future.
|
||||
|
||||
// Update all calendar states at once.
|
||||
try
|
||||
{
|
||||
await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
|
||||
@@ -281,15 +279,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
||||
var calendarViewModels = new List<AccountCalendarViewModel>();
|
||||
|
||||
foreach (var calendar in accountCalendars)
|
||||
{
|
||||
var calendarViewModel = new AccountCalendarViewModel(account, calendar);
|
||||
|
||||
calendarViewModels.Add(calendarViewModel);
|
||||
}
|
||||
|
||||
var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
|
||||
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
|
||||
|
||||
await Dispatcher.ExecuteOnUIThread(() =>
|
||||
@@ -299,9 +289,15 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateCalendarDate(DateTime date)
|
||||
{
|
||||
_navigationDate = date.Date;
|
||||
ForceNavigateCalendarDate();
|
||||
}
|
||||
|
||||
private void ForceNavigateCalendarDate()
|
||||
{
|
||||
var args = new CalendarPageNavigationArgs()
|
||||
var args = new CalendarPageNavigationArgs
|
||||
{
|
||||
NavigationDate = _navigationDate ?? DateTime.Now.Date
|
||||
};
|
||||
@@ -310,73 +306,56 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
_navigationDate = null;
|
||||
}
|
||||
|
||||
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue) { }
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Sync()
|
||||
{
|
||||
// Sync all calendars.
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
|
||||
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = CalendarSynchronizationType.CalendarEvents
|
||||
});
|
||||
|
||||
Messenger.Send(t);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When calendar type switches, we need to navigate to the most ideal date.
|
||||
/// This method returns that date.
|
||||
/// </summary>
|
||||
private DateTime GetDisplayTypeSwitchDate()
|
||||
{
|
||||
var today = _dateContextProvider.GetToday();
|
||||
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||
switch (StatePersistenceService.CalendarDisplayType)
|
||||
{
|
||||
case CalendarDisplayType.Day:
|
||||
if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date;
|
||||
|
||||
return HighlightedDateRange.StartDate;
|
||||
case CalendarDisplayType.Week:
|
||||
if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now))
|
||||
{
|
||||
return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek);
|
||||
}
|
||||
|
||||
return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek);
|
||||
case CalendarDisplayType.WorkWeek:
|
||||
break;
|
||||
case CalendarDisplayType.Month:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DateTime.Today.Date;
|
||||
var referenceRange = CurrentVisibleRange
|
||||
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
|
||||
var targetRange = CalendarRangeResolver.ChangeDisplayType(referenceRange, StatePersistenceService.CalendarDisplayType, settings, today);
|
||||
return targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue);
|
||||
}
|
||||
|
||||
private DateTime? _navigationDate;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly CalendarPageViewModel _calendarPageViewModel;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IUpdateManager _updateManager;
|
||||
private readonly IStoreUpdateService _storeUpdateService;
|
||||
|
||||
#region Commands
|
||||
|
||||
[RelayCommand]
|
||||
private void TodayClicked()
|
||||
{
|
||||
_navigationDate = DateTime.Now.Date;
|
||||
NavigateCalendarDate(_dateContextProvider.GetToday().ToDateTime(TimeOnly.MinValue));
|
||||
}
|
||||
|
||||
ForceNavigateCalendarDate();
|
||||
[RelayCommand]
|
||||
private void PreviousDateRange()
|
||||
{
|
||||
NavigateRelativePeriod(-1);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void NextDateRange()
|
||||
{
|
||||
NavigateRelativePeriod(1);
|
||||
}
|
||||
|
||||
private void NavigateRelativePeriod(int direction)
|
||||
{
|
||||
var today = _dateContextProvider.GetToday();
|
||||
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||
var referenceRange = CurrentVisibleRange
|
||||
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
|
||||
var targetRange = CalendarRangeResolver.Navigate(referenceRange, direction, settings, today);
|
||||
NavigateCalendarDate(targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue));
|
||||
}
|
||||
|
||||
public async Task HandleNavigationItemInvokedAsync(IMenuItem menuItem)
|
||||
@@ -421,7 +400,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
}
|
||||
|
||||
var pickingResult = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups);
|
||||
|
||||
if (pickingResult.ShouldNavigateToCalendarSettings)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.CalendarSettingsPage);
|
||||
@@ -456,17 +434,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
|
||||
{
|
||||
_navigationDate = clickedDateArgs.ClickedDate;
|
||||
|
||||
ForceNavigateCalendarDate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
=> NavigateCalendarDate(clickedDateArgs.ClickedDate);
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
@@ -474,8 +444,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
|
||||
UnregisterRecipients();
|
||||
|
||||
Messenger.Register<VisibleDateRangeChangedMessage>(this);
|
||||
Messenger.Register<CalendarEnableStatusChangedMessage>(this);
|
||||
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
|
||||
Messenger.Register<AccountRemovedMessage>(this);
|
||||
}
|
||||
@@ -484,99 +452,17 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
Messenger.Unregister<VisibleDateRangeChangedMessage>(this);
|
||||
Messenger.Unregister<CalendarEnableStatusChangedMessage>(this);
|
||||
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
|
||||
Messenger.Unregister<AccountRemovedMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the header navigation items based on visible date range and calendar type.
|
||||
/// </summary>
|
||||
private void UpdateDateNavigationHeaderItems()
|
||||
{
|
||||
var settings = PreferencesService.GetCurrentCalendarSettings();
|
||||
var cultureInfo = settings.CultureInfo ?? CultureInfo.CurrentUICulture;
|
||||
|
||||
var visibleRange = HighlightedDateRange ?? new DateRange(DateTime.Today, DateTime.Today.AddDays(1));
|
||||
var headerText = GetHeaderText(visibleRange, cultureInfo);
|
||||
|
||||
DateNavigationHeaderItems.ReplaceRange([headerText]);
|
||||
var headerText = VisibleDateRangeText;
|
||||
DateNavigationHeaderItems.ReplaceRange(string.IsNullOrWhiteSpace(headerText) ? [] : [headerText]);
|
||||
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
private string GetHeaderText(DateRange visibleRange, CultureInfo cultureInfo)
|
||||
{
|
||||
var startDate = visibleRange.StartDate.Date;
|
||||
var endDate = visibleRange.EndDate.Date > startDate ? visibleRange.EndDate.Date.AddDays(-1) : startDate;
|
||||
|
||||
switch (StatePersistenceService.CalendarDisplayType)
|
||||
{
|
||||
case CalendarDisplayType.Day:
|
||||
return startDate.ToString("MMMM d, dddd", cultureInfo);
|
||||
case CalendarDisplayType.Week:
|
||||
case CalendarDisplayType.WorkWeek:
|
||||
if (startDate.Month == endDate.Month && startDate.Year == endDate.Year)
|
||||
{
|
||||
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("%d", cultureInfo)}";
|
||||
}
|
||||
|
||||
return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("MMMM d", cultureInfo)}";
|
||||
case CalendarDisplayType.Month:
|
||||
return GetDominantMonthHeaderText(startDate, endDate, cultureInfo);
|
||||
default:
|
||||
return startDate.ToString("d", cultureInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDominantMonthHeaderText(DateTime startDate, DateTime endDate, CultureInfo cultureInfo)
|
||||
{
|
||||
if (endDate < startDate)
|
||||
{
|
||||
endDate = startDate;
|
||||
}
|
||||
|
||||
var monthDayCounts = new Dictionary<(int Year, int Month), int>();
|
||||
|
||||
for (var day = startDate; day <= endDate; day = day.AddDays(1))
|
||||
{
|
||||
var key = (day.Year, day.Month);
|
||||
|
||||
if (monthDayCounts.TryGetValue(key, out var count))
|
||||
{
|
||||
monthDayCounts[key] = count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
monthDayCounts[key] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
var dominantKey = (Year: startDate.Year, Month: startDate.Month);
|
||||
var dominantCount = -1;
|
||||
|
||||
foreach (var pair in monthDayCounts)
|
||||
{
|
||||
if (pair.Value > dominantCount)
|
||||
{
|
||||
dominantCount = pair.Value;
|
||||
dominantKey = pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
return new DateTime(dominantKey.Year, dominantKey.Month, 1).ToString("Y", cultureInfo);
|
||||
}
|
||||
|
||||
partial void OnHighlightedDateRangeChanged(DateRange value)
|
||||
{
|
||||
UpdateDateNavigationHeaderItems();
|
||||
}
|
||||
|
||||
public async void Receive(CalendarEnableStatusChangedMessage message)
|
||||
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
|
||||
|
||||
public void Receive(CalendarDisplayTypeChangedMessage message)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsVerticalCalendar));
|
||||
@@ -615,11 +501,11 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
var exists = AccountCalendarStateService.AllCalendars
|
||||
.Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value);
|
||||
|
||||
if (exists)
|
||||
return;
|
||||
|
||||
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
|
||||
PreferencesService.DefaultNewEventCalendarId = null;
|
||||
if (!exists)
|
||||
{
|
||||
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
|
||||
PreferencesService.DefaultNewEventCalendarId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
|
||||
@@ -650,12 +536,3 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Trigger to load more data.
|
||||
/// </summary>
|
||||
public enum CalendarInitInitiative
|
||||
{
|
||||
User,
|
||||
App
|
||||
}
|
||||
@@ -49,9 +49,12 @@ public interface ICalendarShellClient : IShellClient
|
||||
IStatePersistanceService StatePersistenceService { get; }
|
||||
IEnumerable DateNavigationHeaderItems { get; }
|
||||
int SelectedDateNavigationHeaderIndex { get; }
|
||||
DateRange? HighlightedDateRange { get; }
|
||||
VisibleDateRange? CurrentVisibleRange { get; }
|
||||
string VisibleDateRangeText { get; }
|
||||
ICommand TodayClickedCommand { get; }
|
||||
ICommand DateClickedCommand { get; }
|
||||
ICommand PreviousDateRangeCommand { get; }
|
||||
ICommand NextDateRangeCommand { get; }
|
||||
IEnumerable GroupedAccountCalendars { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public readonly record struct CalendarDisplayRequest(CalendarDisplayType DisplayType, DateOnly AnchorDate);
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public static class CalendarRangeResolver
|
||||
{
|
||||
public static VisibleDateRange Resolve(CalendarDisplayRequest request, CalendarSettings settings, DateOnly today)
|
||||
{
|
||||
var startDate = GetStartDate(request.DisplayType, request.AnchorDate, settings);
|
||||
var endDate = GetEndDate(request.DisplayType, request.AnchorDate, startDate, settings);
|
||||
var dayCount = endDate.DayNumber - startDate.DayNumber + 1;
|
||||
var dates = Enumerable.Range(0, dayCount)
|
||||
.Select(offset => startDate.AddDays(offset))
|
||||
.ToArray();
|
||||
|
||||
return new VisibleDateRange(
|
||||
request.DisplayType,
|
||||
request.AnchorDate,
|
||||
startDate,
|
||||
endDate,
|
||||
request.AnchorDate,
|
||||
dayCount,
|
||||
today >= startDate && today <= endDate,
|
||||
startDate.Year == endDate.Year && startDate.Month == endDate.Month,
|
||||
dates);
|
||||
}
|
||||
|
||||
public static VisibleDateRange ChangeDisplayType(VisibleDateRange currentRange, CalendarDisplayType targetDisplayType, CalendarSettings settings, DateOnly today)
|
||||
{
|
||||
if (currentRange.DisplayType == targetDisplayType)
|
||||
{
|
||||
return currentRange;
|
||||
}
|
||||
|
||||
var anchorDate = currentRange.AnchorDate;
|
||||
|
||||
if (currentRange.DisplayType == CalendarDisplayType.Month)
|
||||
{
|
||||
anchorDate = currentRange.Contains(today) ? today : currentRange.StartDate;
|
||||
}
|
||||
|
||||
return Resolve(new CalendarDisplayRequest(targetDisplayType, anchorDate), settings, today);
|
||||
}
|
||||
|
||||
public static VisibleDateRange Navigate(VisibleDateRange currentRange, int direction, CalendarSettings settings, DateOnly today)
|
||||
{
|
||||
if (direction == 0)
|
||||
{
|
||||
return currentRange;
|
||||
}
|
||||
|
||||
var normalizedDirection = Math.Sign(direction);
|
||||
var anchorDate = currentRange.DisplayType switch
|
||||
{
|
||||
CalendarDisplayType.Day => currentRange.AnchorDate.AddDays(normalizedDirection),
|
||||
CalendarDisplayType.Week => currentRange.AnchorDate.AddDays(7 * normalizedDirection),
|
||||
CalendarDisplayType.WorkWeek => currentRange.AnchorDate.AddDays(7 * normalizedDirection),
|
||||
CalendarDisplayType.Month => currentRange.AnchorDate.AddMonths(normalizedDirection),
|
||||
_ => currentRange.AnchorDate
|
||||
};
|
||||
|
||||
return Resolve(new CalendarDisplayRequest(currentRange.DisplayType, anchorDate), settings, today);
|
||||
}
|
||||
|
||||
private static DateOnly GetStartDate(CalendarDisplayType displayType, DateOnly anchorDate, CalendarSettings settings)
|
||||
{
|
||||
return displayType switch
|
||||
{
|
||||
CalendarDisplayType.Day => anchorDate,
|
||||
CalendarDisplayType.Week => GetStartOfWeek(anchorDate, settings.FirstDayOfWeek),
|
||||
CalendarDisplayType.WorkWeek => GetStartOfWorkWeek(anchorDate, settings),
|
||||
CalendarDisplayType.Month => new DateOnly(anchorDate.Year, anchorDate.Month, 1),
|
||||
_ => anchorDate
|
||||
};
|
||||
}
|
||||
|
||||
private static DateOnly GetEndDate(CalendarDisplayType displayType, DateOnly anchorDate, DateOnly startDate, CalendarSettings settings)
|
||||
{
|
||||
return displayType switch
|
||||
{
|
||||
CalendarDisplayType.Day => anchorDate,
|
||||
CalendarDisplayType.Week => startDate.AddDays(6),
|
||||
CalendarDisplayType.WorkWeek => startDate.AddDays(settings.WorkWeekDayCount - 1),
|
||||
CalendarDisplayType.Month => new DateOnly(anchorDate.Year, anchorDate.Month, DateTime.DaysInMonth(anchorDate.Year, anchorDate.Month)),
|
||||
_ => anchorDate
|
||||
};
|
||||
}
|
||||
|
||||
private static DateOnly GetStartOfWeek(DateOnly date, DayOfWeek firstDayOfWeek)
|
||||
{
|
||||
var offset = ((int)date.DayOfWeek - (int)firstDayOfWeek + 7) % 7;
|
||||
return date.AddDays(-offset);
|
||||
}
|
||||
|
||||
private static DateOnly GetStartOfWorkWeek(DateOnly anchorDate, CalendarSettings settings)
|
||||
{
|
||||
var startOfWeek = GetStartOfWeek(anchorDate, settings.FirstDayOfWeek);
|
||||
var offsetToWorkWeekStart = settings.GetWeekOffset(settings.WorkWeekStart);
|
||||
return startOfWeek.AddDays(offsetToWorkWeekStart);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public sealed class CalendarRangeTextFormatter : ICalendarRangeTextFormatter
|
||||
{
|
||||
public string Format(VisibleDateRange range, IDateContextProvider dateContextProvider)
|
||||
{
|
||||
var culture = dateContextProvider.Culture;
|
||||
var startText = FormatDate(range.StartDate, culture);
|
||||
|
||||
if (range.DisplayType == CalendarDisplayType.Day)
|
||||
{
|
||||
return startText;
|
||||
}
|
||||
|
||||
return $"{startText} - {FormatDate(range.EndDate, culture)}";
|
||||
}
|
||||
|
||||
private static string FormatDate(DateOnly date, CultureInfo culture)
|
||||
=> date.ToString(culture.DateTimeFormat.ShortDatePattern, culture);
|
||||
}
|
||||
@@ -7,12 +7,33 @@ namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public record CalendarSettings(DayOfWeek FirstDayOfWeek,
|
||||
List<DayOfWeek> WorkingDays,
|
||||
DayOfWeek WorkWeekStart,
|
||||
DayOfWeek WorkWeekEnd,
|
||||
TimeSpan WorkingHourStart,
|
||||
TimeSpan WorkingHourEnd,
|
||||
double HourHeight,
|
||||
DayHeaderDisplayType DayHeaderDisplayType,
|
||||
CultureInfo CultureInfo)
|
||||
{
|
||||
public int WorkWeekDayCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var startOffset = GetWeekOffset(WorkWeekStart);
|
||||
var endOffset = GetWeekOffset(WorkWeekEnd);
|
||||
|
||||
if (endOffset < startOffset)
|
||||
{
|
||||
endOffset += 7;
|
||||
}
|
||||
|
||||
return (endOffset - startOffset) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetWeekOffset(DayOfWeek dayOfWeek)
|
||||
=> ((int)dayOfWeek - (int)FirstDayOfWeek + 7) % 7;
|
||||
|
||||
public TimeSpan? GetTimeSpan(string selectedTime)
|
||||
{
|
||||
// Regardless of the format, we need to parse the time to a TimeSpan.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public interface ICalendarRangeTextFormatter
|
||||
{
|
||||
string Format(VisibleDateRange range, IDateContextProvider dateContextProvider);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public interface IDateContextProvider
|
||||
{
|
||||
CultureInfo Culture { get; }
|
||||
TimeZoneInfo TimeZone { get; }
|
||||
DateOnly GetToday();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public sealed class SystemDateContextProvider : IDateContextProvider
|
||||
{
|
||||
public CultureInfo Culture => CultureInfo.CurrentCulture;
|
||||
|
||||
public TimeZoneInfo TimeZone => TimeZoneInfo.Local;
|
||||
|
||||
public DateOnly GetToday()
|
||||
{
|
||||
var localNow = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, TimeZone);
|
||||
return DateOnly.FromDateTime(localNow.DateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Itenso.TimePeriod;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public sealed record VisibleDateRange(
|
||||
CalendarDisplayType DisplayType,
|
||||
DateOnly AnchorDate,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
DateOnly PrimaryDate,
|
||||
int DayCount,
|
||||
bool ContainsToday,
|
||||
bool SpansSingleMonth,
|
||||
IReadOnlyList<DateOnly> Dates)
|
||||
{
|
||||
public DateRange ToDateRangeExclusive()
|
||||
=> new(StartDate.ToDateTime(TimeOnly.MinValue), EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue));
|
||||
|
||||
public ITimePeriod ToTimePeriod()
|
||||
=> new TimeRange(StartDate.ToDateTime(TimeOnly.MinValue), EndDate.AddDays(1).ToDateTime(TimeOnly.MinValue));
|
||||
|
||||
public bool Contains(DateOnly date)
|
||||
=> date >= StartDate && date <= EndDate;
|
||||
|
||||
public bool Contains(DateTime date)
|
||||
=> Contains(DateOnly.FromDateTime(date));
|
||||
|
||||
public static VisibleDateRange FromDateRange(CalendarDisplayType displayType, DateRange dateRange, DateOnly anchorDate, DateOnly today)
|
||||
{
|
||||
var startDate = DateOnly.FromDateTime(dateRange.StartDate);
|
||||
var endDate = DateOnly.FromDateTime(dateRange.EndDate.AddDays(-1));
|
||||
var dayCount = endDate.DayNumber - startDate.DayNumber + 1;
|
||||
var dates = Enumerable.Range(0, dayCount)
|
||||
.Select(offset => startDate.AddDays(offset))
|
||||
.ToArray();
|
||||
|
||||
return new VisibleDateRange(
|
||||
displayType,
|
||||
anchorDate,
|
||||
startDate,
|
||||
endDate,
|
||||
anchorDate,
|
||||
dayCount,
|
||||
today >= startDate && today <= endDate,
|
||||
startDate.Year == endDate.Year && startDate.Month == endDate.Month,
|
||||
dates);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public static class SettingsNavigationInfoProvider
|
||||
Translator.WinoAccount_SettingsSection_Title,
|
||||
Translator.WinoAccount_SettingsSection_Description,
|
||||
"\uE77B",
|
||||
searchKeywords: Translator.SettingsSearch_WinoAccount_Keywords),
|
||||
searchKeywords: string.Empty),
|
||||
new(null, Translator.SettingsOptions_GeneralSection, string.Empty, "\uE713", isSeparator: true),
|
||||
new(WinoPage.AppPreferencesPage,
|
||||
Translator.SettingsAppPreferences_Title,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"AccountCreationDialog_Initializing": "initializing",
|
||||
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
|
||||
"AccountCreationDialog_SigninIn": "Account information is being saved.",
|
||||
"Purchased": "Purchased",
|
||||
"AccountEditDialog_Message": "Account Name",
|
||||
"AccountEditDialog_Title": "Edit Account",
|
||||
"AccountPickerDialog_Title": "Pick an account",
|
||||
@@ -1146,6 +1147,56 @@
|
||||
"WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later",
|
||||
"WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11",
|
||||
"WelcomeWizard_Step1Title": "Welcome",
|
||||
"SystemTrayMenu_Open": "Open",
|
||||
"WinoAccount_Titlebar_SyncBenefitTitle": "Sync settings",
|
||||
"WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.",
|
||||
"WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons",
|
||||
"WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.",
|
||||
"WinoAccount_Management_Description": "Manage your Wino Account, AI Pack access, and synchronized settings.",
|
||||
"WinoAccount_Management_SignedOutTitle": "Sign in to Wino Mail",
|
||||
"WinoAccount_Management_SignedOutDescription": "Sign in or create an account to sync your email, access AI features, and manage your settings across devices.",
|
||||
"WinoAccount_Management_ProfileSectionHeader": "Profile",
|
||||
"WinoAccount_Management_AddOnsSectionHeader": "Wino Add-Ons",
|
||||
"WinoAccount_Management_DataSectionHeader": "Data",
|
||||
"WinoAccount_Management_AccountActionsSectionHeader": "Account actions",
|
||||
"WinoAccount_Management_AccountCardTitle": "Account",
|
||||
"WinoAccount_Management_AccountCardDescription": "Your Wino Account email address and current account state.",
|
||||
"WinoAccount_Management_AiPackCardTitle": "AI Pack",
|
||||
"WinoAccount_Management_AiPackCardDescription": "See whether Wino AI Pack is active and how much usage is left.",
|
||||
"WinoAccount_Management_AiPackActive": "AI Pack is active",
|
||||
"WinoAccount_Management_AiPackInactive": "AI Pack is not active",
|
||||
"WinoAccount_Management_AiPackUsage": "{0} of {1} uses consumed. {2} remaining.",
|
||||
"WinoAccount_Management_AiPackBillingPeriod": "Billing period: {0:d} - {1:d}",
|
||||
"WinoAccount_Management_AiPackUnknownUsage": "Usage details are not available yet.",
|
||||
"WinoAccount_Management_AiPackBuyDescription": "Buy Wino AI Pack to translate, rewrite or summarize emails with AI.",
|
||||
"WinoAccount_Management_AiPackPromoTitle": "Unlock AI Pack",
|
||||
"WinoAccount_Management_AiPackPromoDescription": "Supercharge your email workflow with AI-powered tools. Translate messages into 50+ languages, rewrite for clarity and tone, and get instant summaries of long threads.",
|
||||
"WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo",
|
||||
"WinoAccount_Management_AiPackPromoRequests": "1,000 requests",
|
||||
"WinoAccount_Management_AiPackGetButton": "Get AI Pack",
|
||||
"WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.",
|
||||
"WinoAccount_Management_PurchaseStartFailed": "Wino could not start the checkout session for this add-on.",
|
||||
"WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active",
|
||||
"WinoAccount_Management_AiPackRenews": "Renews {0:d}",
|
||||
"WinoAccount_Management_AiPackRequestsUsed": "Requests used this month",
|
||||
"WinoAccount_Management_AiPackResets": "Resets {0:d}",
|
||||
"WinoAccount_Management_AiPackFeatureTranslate": "Translate",
|
||||
"WinoAccount_Management_AiPackFeatureRewrite": "Rewrite",
|
||||
"WinoAccount_Management_AiPackFeatureSummarize": "Summarize",
|
||||
"WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences",
|
||||
"WinoAccount_Management_SyncPreferencesDescription": "Import or export your preferences to cloud. Import them across devices.",
|
||||
"WinoAccount_Management_SignOutTitle": "Sign out",
|
||||
"WinoAccount_Management_SignOutDescription": "Sign out of your account on this device",
|
||||
"WinoAccount_Management_StatusLabel": "Status: {0}",
|
||||
"WinoAccount_Management_NoRemoteSettings": "There are no synchronized settings stored for this account yet.",
|
||||
"WinoAccount_Management_ExportSucceeded": "Your settings were exported to your Wino Account.",
|
||||
"WinoAccount_Management_ImportSucceeded": "Imported {0} settings from your Wino Account.",
|
||||
"WinoAccount_Management_ImportPartial": "Imported {0} settings. {1} settings could not be restored.",
|
||||
"WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.",
|
||||
"WinoAccount_Management_EmptyExport": "There are no preference values to export.",
|
||||
"WinoAccount_Management_ImportEmpty": "The synchronized settings payload does not contain any values to restore.",
|
||||
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
|
||||
"WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.",
|
||||
"WinoAccount_SettingsSection_Title": "Wino Account",
|
||||
"WinoAccount_SettingsSection_Description": "Create or sign in to a Wino Account using your localhost auth service.",
|
||||
"WinoAccount_RegisterButton_Title": "Register account",
|
||||
@@ -1168,6 +1219,10 @@
|
||||
"WinoAccount_RegisterDialog_DifferenceTitle": "Wino Account is separate from your mail accounts",
|
||||
"WinoAccount_RegisterDialog_DifferenceDescription": "Your Outlook, Gmail, IMAP, or other email accounts stay exactly as they are. A Wino Account only manages Wino-specific features and account-based add-ons.",
|
||||
"WinoAccount_RegisterDialog_PrimaryButton": "Register",
|
||||
"WinoAccount_RegisterDialog_PrivacyTitle": "Privacy and API processing",
|
||||
"WinoAccount_RegisterDialog_PrivacyDescription": "Optional add-ons such as Wino AI Pack may send selected email HTML content to the Wino API service only when you use those features.",
|
||||
"WinoAccount_RegisterDialog_PrivacyLinkText": "Read the privacy policy",
|
||||
"WinoAccount_RegisterDialog_PrivacyCheckbox": "I agree to the privacy policy.",
|
||||
"WinoAccount_LoginDialog_Title": "Sign In to Wino Account",
|
||||
"WinoAccount_LoginDialog_Description": "Sign in to your Wino Account to sync your Wino setup and access account-based features.",
|
||||
"WinoAccount_LoginDialog_HeroTitle": "Welcome back",
|
||||
@@ -1175,17 +1230,28 @@
|
||||
"WinoAccount_LoginDialog_BenefitsDescription": "Use your Wino Account to continue syncing settings across devices and to access paid add-ons such as Wino AI Pack.",
|
||||
"WinoAccount_LoginDialog_DifferenceTitle": "This is not your email mailbox sign-in",
|
||||
"WinoAccount_LoginDialog_DifferenceDescription": "Signing in here does not add or replace your Outlook, Gmail, or IMAP accounts in Wino. It only signs you in to Wino-specific services.",
|
||||
"WinoAccount_LoginDialog_ForgotPasswordLink": "Forgot password?",
|
||||
"WinoAccount_EmailLabel": "Email",
|
||||
"WinoAccount_EmailPlaceholder": "name@example.com",
|
||||
"WinoAccount_PasswordLabel": "Password",
|
||||
"WinoAccount_ConfirmPasswordLabel": "Confirm password",
|
||||
"WinoAccount_ForgotPasswordDialog_Title": "Reset your password",
|
||||
"WinoAccount_ForgotPasswordDialog_PrimaryButton": "Send reset email",
|
||||
"WinoAccount_ForgotPasswordDialog_BackToSignIn": "Back to sign in",
|
||||
"WinoAccount_ForgotPasswordDialog_Description": "Enter your Wino Account email address and we will send you a password reset link if the address is registered.",
|
||||
"WinoAccount_Validation_EmailRequired": "Email is required.",
|
||||
"WinoAccount_Validation_PasswordRequired": "Password is required.",
|
||||
"WinoAccount_Validation_PasswordMismatch": "Passwords do not match.",
|
||||
"WinoAccount_Validation_PrivacyConsentRequired": "You must accept the privacy policy before creating a Wino Account.",
|
||||
"WinoAccount_Error_InvalidCredentials": "The email address or password is incorrect.",
|
||||
"WinoAccount_Error_AccountLocked": "This account is temporarily locked.",
|
||||
"WinoAccount_Error_AccountBanned": "This account has been banned.",
|
||||
"WinoAccount_Error_AccountSuspended": "This account has been suspended.",
|
||||
"WinoAccount_Error_EmailNotConfirmed": "Please confirm your email address before signing in.",
|
||||
"WinoAccount_Error_EmailConfirmationRequired": "Please confirm your email address before signing in.",
|
||||
"WinoAccount_Error_EmailConfirmationResendNotAvailable": "A new confirmation email is not available yet.",
|
||||
"WinoAccount_Error_EmailConfirmationResendInvalid": "This confirmation request is no longer valid. Please try signing in again.",
|
||||
"WinoAccount_Error_EmailNotRegistered": "This email address is not registered.",
|
||||
"WinoAccount_Error_RefreshTokenInvalid": "Your session is no longer valid. Please sign in again.",
|
||||
"WinoAccount_Error_EmailAlreadyRegistered": "This email address is already registered.",
|
||||
"WinoAccount_Error_ExternalLoginEmailRequired": "An email address is required to complete external sign-in.",
|
||||
@@ -1196,14 +1262,25 @@
|
||||
"WinoAccount_Error_ValidationFailed": "The request is invalid. Please review the entered values.",
|
||||
"WinoAccount_RegisterSuccessMessage": "Wino Account registration completed for {0}.",
|
||||
"WinoAccount_LoginSuccessMessage": "Signed in to Wino Account as {0}.",
|
||||
"WinoAccount_EmailConfirmationSentDialog_Title": "Confirm your email address",
|
||||
"WinoAccount_EmailConfirmationSentDialog_Message": "We sent an email confirmation to {0}. Please confirm it and try signing in again.",
|
||||
"WinoAccount_EmailConfirmationPendingDialog_Title": "Email confirmation required",
|
||||
"WinoAccount_EmailConfirmationPendingDialog_Message": "We are still waiting for you to confirm {0}.",
|
||||
"WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Resend confirmation email",
|
||||
"WinoAccount_EmailConfirmationPendingDialog_Countdown": "You can resend the confirmation email in {0}.",
|
||||
"WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "You can resend the confirmation email now.",
|
||||
"WinoAccount_EmailConfirmationResentDialog_Title": "Confirmation email resent",
|
||||
"WinoAccount_EmailConfirmationResentDialog_Message": "We sent another confirmation email to {0}. Please confirm it and try signing in again.",
|
||||
"WinoAccount_ForgotPasswordDialog_SuccessTitle": "Password reset email sent",
|
||||
"WinoAccount_ForgotPasswordDialog_SuccessMessage": "We sent a password reset email to {0}. Open that message to choose a new password.",
|
||||
"WinoAccount_ChangePassword_Title": "Change password",
|
||||
"WinoAccount_ChangePassword_Description": "Send a password reset email to this Wino Account.",
|
||||
"WinoAccount_ChangePassword_Action": "Send reset email",
|
||||
"WinoAccount_ChangePassword_ConfirmationMessage": "Do you want Wino to send a password reset email to {0}?",
|
||||
"WinoAccount_SignOut_SuccessMessage": "Signed out from Wino Account {0}.",
|
||||
"WinoAccount_SignOut_NoAccountMessage": "There is no active Wino Account to sign out.",
|
||||
"WinoAccount_Titlebar_SignedOutTitle": "Wino Account",
|
||||
"WinoAccount_Titlebar_SignedOutDescription": "Sign in or create a Wino Account to manage your Wino session.",
|
||||
"WinoAccount_Titlebar_SyncBenefitTitle": "Sync settings",
|
||||
"WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.",
|
||||
"WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons",
|
||||
"WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.",
|
||||
"WinoAccount_Titlebar_SignedInStatus": "Status: {0}",
|
||||
"WelcomeWizard_Step2Title": "Add Account",
|
||||
"WelcomeWizard_Step3Title": "Finish Setup",
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using CommunityToolkit.Mvvm.Collections;
|
||||
using FluentAssertions;
|
||||
using Itenso.TimePeriod;
|
||||
using Moq;
|
||||
using Wino.Calendar.ViewModels;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Calendar.ViewModels.Interfaces;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests;
|
||||
|
||||
public class CalendarPageViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ApplyDisplayRequestAsync_UpdatesVisibleRangeAndThreePeriodLoadWindow()
|
||||
{
|
||||
var settings = CreateSettings(firstDayOfWeek: DayOfWeek.Monday);
|
||||
var today = new DateOnly(2026, 3, 20);
|
||||
var preferencesService = CreatePreferencesService(settings);
|
||||
var calendarService = new Mock<ICalendarService>();
|
||||
ITimePeriod? requestedPeriod = null;
|
||||
|
||||
calendarService
|
||||
.Setup(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()))
|
||||
.Callback<IAccountCalendar, ITimePeriod>((_, period) => requestedPeriod = period)
|
||||
.ReturnsAsync([]);
|
||||
|
||||
var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, today);
|
||||
var request = new CalendarDisplayRequest(CalendarDisplayType.Week, new DateOnly(2026, 3, 18));
|
||||
|
||||
await viewModel.ApplyDisplayRequestAsync(request);
|
||||
|
||||
viewModel.CurrentVisibleRange.StartDate.Should().Be(new DateOnly(2026, 3, 16));
|
||||
viewModel.CurrentVisibleRange.EndDate.Should().Be(new DateOnly(2026, 3, 22));
|
||||
viewModel.LoadedDateWindow.StartDate.Should().Be(new DateTime(2026, 3, 9));
|
||||
viewModel.LoadedDateWindow.EndDate.Should().Be(new DateTime(2026, 3, 30));
|
||||
viewModel.VisibleDateRangeText.Should().Be("3/16/2026 - 3/22/2026");
|
||||
|
||||
requestedPeriod.Should().NotBeNull();
|
||||
requestedPeriod!.Start.Should().Be(new DateTime(2026, 3, 9));
|
||||
requestedPeriod.End.Should().Be(new DateTime(2026, 3, 30));
|
||||
calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyDisplayRequestAsync_DoesNotReloadWhenResolvedRangeIsUnchanged()
|
||||
{
|
||||
var settings = CreateSettings();
|
||||
var preferencesService = CreatePreferencesService(settings);
|
||||
var calendarService = new Mock<ICalendarService>();
|
||||
|
||||
calendarService
|
||||
.Setup(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()))
|
||||
.ReturnsAsync([]);
|
||||
|
||||
var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20));
|
||||
var request = new CalendarDisplayRequest(CalendarDisplayType.Day, new DateOnly(2026, 3, 20));
|
||||
|
||||
await viewModel.ApplyDisplayRequestAsync(request);
|
||||
await viewModel.ApplyDisplayRequestAsync(request);
|
||||
|
||||
calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReloadCurrentVisibleRangeAsync_RecomputesWhenCalendarSettingsChange()
|
||||
{
|
||||
var currentSettings = CreateSettings(firstDayOfWeek: DayOfWeek.Monday);
|
||||
var preferencesService = CreatePreferencesService(() => currentSettings);
|
||||
var calendarService = new Mock<ICalendarService>();
|
||||
|
||||
calendarService
|
||||
.Setup(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()))
|
||||
.ReturnsAsync([]);
|
||||
|
||||
var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20));
|
||||
var request = new CalendarDisplayRequest(CalendarDisplayType.Week, new DateOnly(2026, 3, 18));
|
||||
|
||||
await viewModel.ApplyDisplayRequestAsync(request);
|
||||
viewModel.CurrentVisibleRange.StartDate.Should().Be(new DateOnly(2026, 3, 16));
|
||||
|
||||
currentSettings = CreateSettings(firstDayOfWeek: DayOfWeek.Sunday);
|
||||
await viewModel.ReloadCurrentVisibleRangeAsync();
|
||||
|
||||
viewModel.CurrentVisibleRange.StartDate.Should().Be(new DateOnly(2026, 3, 15));
|
||||
calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny<IAccountCalendar>(), It.IsAny<ITimePeriod>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
private static CalendarPageViewModel CreateViewModel(
|
||||
ICalendarService calendarService,
|
||||
IPreferencesService preferencesService,
|
||||
DateOnly today)
|
||||
{
|
||||
var account = new MailAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Primary",
|
||||
SenderName = "Primary",
|
||||
Address = "primary@example.com",
|
||||
ProviderType = MailProviderType.Outlook
|
||||
};
|
||||
|
||||
var calendar = new AccountCalendar
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = account.Id,
|
||||
Name = "Calendar",
|
||||
RemoteCalendarId = "calendar",
|
||||
SynchronizationDeltaToken = string.Empty,
|
||||
TextColorHex = "#000000",
|
||||
BackgroundColorHex = "#ffffff",
|
||||
TimeZone = TimeZoneInfo.Utc.Id,
|
||||
IsExtended = true,
|
||||
IsPrimary = true,
|
||||
IsSynchronizationEnabled = true
|
||||
};
|
||||
|
||||
var accountCalendarViewModel = new AccountCalendarViewModel(account, calendar);
|
||||
var accountCalendarStateService = new FakeAccountCalendarStateService([accountCalendarViewModel]);
|
||||
|
||||
var statePersistenceService = new Mock<IStatePersistanceService>();
|
||||
statePersistenceService.SetupAllProperties();
|
||||
statePersistenceService.Object.ApplicationMode = WinoApplicationMode.Calendar;
|
||||
statePersistenceService.Object.CalendarDisplayType = CalendarDisplayType.Week;
|
||||
|
||||
return new CalendarPageViewModel(
|
||||
statePersistenceService.Object,
|
||||
calendarService,
|
||||
Mock.Of<INavigationService>(),
|
||||
Mock.Of<IKeyPressService>(),
|
||||
Mock.Of<INativeAppService>(),
|
||||
accountCalendarStateService,
|
||||
preferencesService,
|
||||
Mock.Of<IWinoRequestDelegator>(),
|
||||
Mock.Of<IMailDialogService>(),
|
||||
new TestDateContextProvider("en-US", today),
|
||||
new CalendarRangeTextFormatter());
|
||||
}
|
||||
|
||||
private static Mock<IPreferencesService> CreatePreferencesService(CalendarSettings settings)
|
||||
=> CreatePreferencesService(() => settings);
|
||||
|
||||
private static Mock<IPreferencesService> CreatePreferencesService(Func<CalendarSettings> settingsFactory)
|
||||
{
|
||||
var preferencesService = new Mock<IPreferencesService>();
|
||||
preferencesService.Setup(service => service.GetCurrentCalendarSettings()).Returns(settingsFactory);
|
||||
return preferencesService;
|
||||
}
|
||||
|
||||
private static CalendarSettings CreateSettings(
|
||||
DayOfWeek firstDayOfWeek = DayOfWeek.Monday,
|
||||
DayOfWeek workWeekStart = DayOfWeek.Monday,
|
||||
DayOfWeek workWeekEnd = DayOfWeek.Friday,
|
||||
string cultureName = "en-US")
|
||||
{
|
||||
return new CalendarSettings(
|
||||
firstDayOfWeek,
|
||||
[DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday],
|
||||
workWeekStart,
|
||||
workWeekEnd,
|
||||
TimeSpan.FromHours(8),
|
||||
TimeSpan.FromHours(17),
|
||||
64,
|
||||
DayHeaderDisplayType.TwentyFourHour,
|
||||
CultureInfo.GetCultureInfo(cultureName));
|
||||
}
|
||||
|
||||
private sealed class FakeAccountCalendarStateService : IAccountCalendarStateService
|
||||
{
|
||||
private readonly List<AccountCalendarViewModel> _calendars;
|
||||
private readonly ObservableCollection<GroupedAccountCalendarViewModel> _groupedCalendars = [];
|
||||
|
||||
public FakeAccountCalendarStateService(IEnumerable<AccountCalendarViewModel> calendars)
|
||||
{
|
||||
_calendars = calendars.ToList();
|
||||
GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_groupedCalendars);
|
||||
}
|
||||
|
||||
public IDispatcher Dispatcher { get; set; } = null!;
|
||||
public ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
|
||||
|
||||
public event EventHandler<GroupedAccountCalendarViewModel>? CollectiveAccountGroupSelectionStateChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event EventHandler<AccountCalendarViewModel>? AccountCalendarSelectionStateChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public IEnumerable<AccountCalendarViewModel> ActiveCalendars => _calendars;
|
||||
public IEnumerable<AccountCalendarViewModel> AllCalendars => _calendars;
|
||||
public ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; } = null!;
|
||||
|
||||
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar) => _groupedCalendars.Add(groupedAccountCalendar);
|
||||
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar) => _groupedCalendars.Remove(groupedAccountCalendar);
|
||||
public void ClearGroupedAccountCalendars() => _groupedCalendars.Clear();
|
||||
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar) => _calendars.Add(accountCalendar);
|
||||
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar) => _calendars.Remove(accountCalendar);
|
||||
}
|
||||
|
||||
private sealed class TestDateContextProvider(string cultureName, DateOnly today) : IDateContextProvider
|
||||
{
|
||||
public CultureInfo Culture => CultureInfo.GetCultureInfo(cultureName);
|
||||
public TimeZoneInfo TimeZone => TimeZoneInfo.Utc;
|
||||
public DateOnly GetToday() => today;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using System.Globalization;
|
||||
using FluentAssertions;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests;
|
||||
|
||||
public class CalendarRangeResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resolve_Day_ReturnsAnchorDateOnly()
|
||||
{
|
||||
var settings = CreateSettings();
|
||||
var today = new DateOnly(2026, 3, 20);
|
||||
|
||||
var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Day, today), settings, today);
|
||||
|
||||
range.StartDate.Should().Be(today);
|
||||
range.EndDate.Should().Be(today);
|
||||
range.DayCount.Should().Be(1);
|
||||
range.ContainsToday.Should().BeTrue();
|
||||
range.Dates.Should().ContainSingle().Which.Should().Be(today);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Week_HonorsConfiguredFirstDayOfWeek()
|
||||
{
|
||||
var settings = CreateSettings(firstDayOfWeek: DayOfWeek.Sunday);
|
||||
var anchor = new DateOnly(2026, 3, 18);
|
||||
|
||||
var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Week, anchor), settings, today: anchor);
|
||||
|
||||
range.StartDate.Should().Be(new DateOnly(2026, 3, 15));
|
||||
range.EndDate.Should().Be(new DateOnly(2026, 3, 21));
|
||||
range.DayCount.Should().Be(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_WorkWeek_UsesConfiguredBounds()
|
||||
{
|
||||
var settings = CreateSettings(
|
||||
firstDayOfWeek: DayOfWeek.Sunday,
|
||||
workWeekStart: DayOfWeek.Monday,
|
||||
workWeekEnd: DayOfWeek.Thursday);
|
||||
var anchor = new DateOnly(2026, 3, 18);
|
||||
|
||||
var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.WorkWeek, anchor), settings, today: anchor);
|
||||
|
||||
range.StartDate.Should().Be(new DateOnly(2026, 3, 16));
|
||||
range.EndDate.Should().Be(new DateOnly(2026, 3, 19));
|
||||
range.DayCount.Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_Month_CoversEntireAnchorMonth()
|
||||
{
|
||||
var settings = CreateSettings();
|
||||
var anchor = new DateOnly(2026, 2, 14);
|
||||
|
||||
var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Month, anchor), settings, today: anchor);
|
||||
|
||||
range.StartDate.Should().Be(new DateOnly(2026, 2, 1));
|
||||
range.EndDate.Should().Be(new DateOnly(2026, 2, 28));
|
||||
range.DayCount.Should().Be(28);
|
||||
range.SpansSingleMonth.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CalendarDisplayType.Day, 2026, 3, 18, 2026, 3, 19, 2026, 3, 17)]
|
||||
[InlineData(CalendarDisplayType.Week, 2026, 3, 18, 2026, 3, 25, 2026, 3, 11)]
|
||||
[InlineData(CalendarDisplayType.WorkWeek, 2026, 3, 18, 2026, 3, 25, 2026, 3, 11)]
|
||||
[InlineData(CalendarDisplayType.Month, 2026, 3, 18, 2026, 4, 18, 2026, 2, 18)]
|
||||
public void Navigate_MovesExactlyOnePeriod(
|
||||
CalendarDisplayType displayType,
|
||||
int year,
|
||||
int month,
|
||||
int day,
|
||||
int nextYear,
|
||||
int nextMonth,
|
||||
int nextDay,
|
||||
int previousYear,
|
||||
int previousMonth,
|
||||
int previousDay)
|
||||
{
|
||||
var settings = CreateSettings();
|
||||
var today = new DateOnly(2026, 3, 20);
|
||||
var current = CalendarRangeResolver.Resolve(
|
||||
new CalendarDisplayRequest(displayType, new DateOnly(year, month, day)),
|
||||
settings,
|
||||
today);
|
||||
|
||||
var next = CalendarRangeResolver.Navigate(current, 1, settings, today);
|
||||
var previous = CalendarRangeResolver.Navigate(current, -1, settings, today);
|
||||
|
||||
next.AnchorDate.Should().Be(new DateOnly(nextYear, nextMonth, nextDay));
|
||||
previous.AnchorDate.Should().Be(new DateOnly(previousYear, previousMonth, previousDay));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeDisplayType_FromMonth_UsesTodayWhenTodayIsInsideCurrentMonth()
|
||||
{
|
||||
var settings = CreateSettings();
|
||||
var today = new DateOnly(2026, 3, 20);
|
||||
var monthRange = CalendarRangeResolver.Resolve(
|
||||
new CalendarDisplayRequest(CalendarDisplayType.Month, new DateOnly(2026, 3, 5)),
|
||||
settings,
|
||||
today);
|
||||
|
||||
var dayRange = CalendarRangeResolver.ChangeDisplayType(monthRange, CalendarDisplayType.Day, settings, today);
|
||||
|
||||
dayRange.AnchorDate.Should().Be(today);
|
||||
dayRange.StartDate.Should().Be(today);
|
||||
dayRange.EndDate.Should().Be(today);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Formatter_Day_UsesSingleDate()
|
||||
{
|
||||
var formatter = new CalendarRangeTextFormatter();
|
||||
var range = new VisibleDateRange(
|
||||
CalendarDisplayType.Day,
|
||||
new DateOnly(2026, 3, 20),
|
||||
new DateOnly(2026, 3, 20),
|
||||
new DateOnly(2026, 3, 20),
|
||||
new DateOnly(2026, 3, 20),
|
||||
1,
|
||||
true,
|
||||
true,
|
||||
[new DateOnly(2026, 3, 20)]);
|
||||
|
||||
var text = formatter.Format(range, new TestDateContextProvider("en-US", today: new DateOnly(2026, 3, 20)));
|
||||
|
||||
text.Should().Be("3/20/2026");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Formatter_Range_UsesCultureShortDatePattern()
|
||||
{
|
||||
var formatter = new CalendarRangeTextFormatter();
|
||||
var range = new VisibleDateRange(
|
||||
CalendarDisplayType.Week,
|
||||
new DateOnly(2026, 3, 20),
|
||||
new DateOnly(2026, 3, 16),
|
||||
new DateOnly(2026, 3, 22),
|
||||
new DateOnly(2026, 3, 20),
|
||||
7,
|
||||
true,
|
||||
true,
|
||||
[
|
||||
new DateOnly(2026, 3, 16),
|
||||
new DateOnly(2026, 3, 17),
|
||||
new DateOnly(2026, 3, 18),
|
||||
new DateOnly(2026, 3, 19),
|
||||
new DateOnly(2026, 3, 20),
|
||||
new DateOnly(2026, 3, 21),
|
||||
new DateOnly(2026, 3, 22)
|
||||
]);
|
||||
|
||||
var text = formatter.Format(range, new TestDateContextProvider("de-DE", today: new DateOnly(2026, 3, 20)));
|
||||
|
||||
text.Should().Be("16.03.2026 - 22.03.2026");
|
||||
}
|
||||
|
||||
private static CalendarSettings CreateSettings(
|
||||
DayOfWeek firstDayOfWeek = DayOfWeek.Monday,
|
||||
DayOfWeek workWeekStart = DayOfWeek.Monday,
|
||||
DayOfWeek workWeekEnd = DayOfWeek.Friday,
|
||||
string cultureName = "en-US")
|
||||
{
|
||||
return new CalendarSettings(
|
||||
firstDayOfWeek,
|
||||
[DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday],
|
||||
workWeekStart,
|
||||
workWeekEnd,
|
||||
TimeSpan.FromHours(8),
|
||||
TimeSpan.FromHours(17),
|
||||
64,
|
||||
DayHeaderDisplayType.TwentyFourHour,
|
||||
CultureInfo.GetCultureInfo(cultureName));
|
||||
}
|
||||
|
||||
private sealed class TestDateContextProvider(string cultureName, DateOnly today) : IDateContextProvider
|
||||
{
|
||||
public CultureInfo Culture => CultureInfo.GetCultureInfo(cultureName);
|
||||
public TimeZoneInfo TimeZone => TimeZoneInfo.Utc;
|
||||
public DateOnly GetToday() => today;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
|
||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
|
||||
@@ -116,12 +116,9 @@
|
||||
|
||||
<ResourceDictionary Source="Styles/CalendarThemeResources.xaml" />
|
||||
|
||||
<ResourceDictionary Source="Styles/WinoDayTimelineCanvas.xaml" />
|
||||
<ResourceDictionary Source="Styles/WinoCalendarView.xaml" />
|
||||
<ResourceDictionary Source="Styles/WinoCalendarTypeSelectorControl.xaml" />
|
||||
|
||||
<styles:WinoExpanderStyle />
|
||||
<styles:WinoCalendarResources />
|
||||
<ResourceDictionary Source="Styles/CalendarShellNavigationViewStyle.xaml" />
|
||||
<ResourceDictionary Source="AppThemes/Default.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
@@ -283,6 +283,8 @@ public partial class App : WinoApplication,
|
||||
services.AddTransient<IProviderService, ProviderService>();
|
||||
services.AddSingleton<IAuthenticatorConfig, MailAuthenticatorConfiguration>();
|
||||
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
|
||||
services.AddSingleton<IDateContextProvider, SystemDateContextProvider>();
|
||||
services.AddSingleton<ICalendarRangeTextFormatter, CalendarRangeTextFormatter>();
|
||||
}
|
||||
|
||||
private void RegisterViewModels(IServiceCollection services)
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Calendar.Args;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Helpers;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarControl : Control, IDisposable
|
||||
{
|
||||
private const string PART_WinoFlipView = nameof(PART_WinoFlipView);
|
||||
private const string PART_IdleGrid = nameof(PART_IdleGrid);
|
||||
|
||||
public event EventHandler<TimelineCellSelectedArgs>? TimelineCellSelected;
|
||||
public event EventHandler<TimelineCellUnselectedArgs>? TimelineCellUnselected;
|
||||
|
||||
public event EventHandler? ScrollPositionChanging;
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of day ranges to render.
|
||||
/// Each day range usually represents a week, but it may support other ranges.
|
||||
/// </summary>
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ObservableCollection<DayRangeRenderModel>? DayRanges { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty(DefaultValue = -1)]
|
||||
public partial int SelectedFlipViewIndex { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial DayRangeRenderModel? SelectedFlipViewDayRange { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial WinoDayTimelineCanvas? ActiveCanvas { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty(DefaultValue = true)]
|
||||
public partial bool IsFlipIdle { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ScrollViewer? ActiveScrollViewer { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ItemsPanelTemplate? VerticalItemsPanelTemplate { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial ItemsPanelTemplate? HorizontalItemsPanelTemplate { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty(DefaultValue = CalendarOrientation.Horizontal)]
|
||||
public partial CalendarOrientation Orientation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the day-week-month-year display type.
|
||||
/// Orientation is not determined by this property, but Orientation property.
|
||||
/// This property is used to determine the template to use for the calendar.
|
||||
/// </summary>
|
||||
[GeneratedDependencyProperty(DefaultValue = CalendarDisplayType.Day)]
|
||||
public partial CalendarDisplayType DisplayType { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
private WinoCalendarFlipView? InternalFlipView;
|
||||
private Grid? IdleGrid;
|
||||
|
||||
private ScrollViewer? _previousScrollViewer;
|
||||
private WinoDayTimelineCanvas? _previousCanvas;
|
||||
|
||||
public WinoCalendarControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(WinoCalendarControl);
|
||||
SizeChanged += CalendarSizeChanged;
|
||||
}
|
||||
|
||||
partial void OnVerticalItemsPanelTemplateChanged(ItemsPanelTemplate? newValue)
|
||||
=> ManageCalendarOrientation();
|
||||
|
||||
partial void OnHorizontalItemsPanelTemplateChanged(ItemsPanelTemplate? newValue)
|
||||
=> ManageCalendarOrientation();
|
||||
|
||||
partial void OnOrientationChanged(CalendarOrientation newValue)
|
||||
=> ManageCalendarOrientation();
|
||||
|
||||
partial void OnDisplayTypeChanged(CalendarDisplayType newValue)
|
||||
=> ManageDisplayType();
|
||||
|
||||
partial void OnIsFlipIdleChanged(bool newValue)
|
||||
=> UpdateIdleState();
|
||||
|
||||
partial void OnDayRangesChanged(ObservableCollection<DayRangeRenderModel>? newValue)
|
||||
=> EnsureStableSelection();
|
||||
|
||||
partial void OnSelectedFlipViewDayRangeChanged(DayRangeRenderModel? newValue)
|
||||
=> EnsureStableSelection();
|
||||
|
||||
partial void OnActiveScrollViewerPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var newValue = e.NewValue as ScrollViewer;
|
||||
if (_previousScrollViewer != null)
|
||||
{
|
||||
DeregisterScrollChanges(_previousScrollViewer);
|
||||
}
|
||||
|
||||
if (newValue != null)
|
||||
{
|
||||
RegisterScrollChanges(newValue);
|
||||
}
|
||||
|
||||
_previousScrollViewer = newValue;
|
||||
ManageHighlightedDateRange();
|
||||
}
|
||||
|
||||
partial void OnActiveCanvasPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var newValue = e.NewValue as WinoDayTimelineCanvas;
|
||||
if (_previousCanvas != null)
|
||||
{
|
||||
DeregisterCanvas(_previousCanvas);
|
||||
}
|
||||
|
||||
if (newValue != null)
|
||||
{
|
||||
RegisterCanvas(newValue);
|
||||
}
|
||||
|
||||
_previousCanvas = newValue;
|
||||
ManageHighlightedDateRange();
|
||||
}
|
||||
|
||||
private void ManageCalendarOrientation()
|
||||
{
|
||||
if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return;
|
||||
|
||||
InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate;
|
||||
}
|
||||
|
||||
private void ManageDisplayType()
|
||||
{
|
||||
if (InternalFlipView == null) return;
|
||||
|
||||
InternalFlipView.DisplayType = DisplayType;
|
||||
}
|
||||
|
||||
private void ManageHighlightedDateRange()
|
||||
{
|
||||
if (InternalFlipView?.IsProgrammaticNavigationInProgress == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedFlipViewDayRange = InternalFlipView?.SelectedItem as DayRangeRenderModel;
|
||||
}
|
||||
|
||||
private void DeregisterCanvas(WinoDayTimelineCanvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
|
||||
canvas.SelectedDateTime = null;
|
||||
canvas.TimelineCellSelected -= ActiveTimelineCellSelected;
|
||||
canvas.TimelineCellUnselected -= ActiveTimelineCellUnselected;
|
||||
}
|
||||
|
||||
private void RegisterCanvas(WinoDayTimelineCanvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
|
||||
canvas.SelectedDateTime = null;
|
||||
canvas.TimelineCellSelected += ActiveTimelineCellSelected;
|
||||
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;
|
||||
|
||||
ActiveCanvas.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.ProgrammaticNavigationCompleted -= InternalFlipViewProgrammaticNavigationCompleted;
|
||||
|
||||
if (InternalFlipView is IDisposable disposableFlipView)
|
||||
{
|
||||
disposableFlipView.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (_previousScrollViewer != null)
|
||||
{
|
||||
DeregisterScrollChanges(_previousScrollViewer);
|
||||
_previousScrollViewer = null;
|
||||
}
|
||||
|
||||
if (_previousCanvas != null)
|
||||
{
|
||||
DeregisterCanvas(_previousCanvas);
|
||||
_previousCanvas = null;
|
||||
}
|
||||
|
||||
InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView;
|
||||
IdleGrid = GetTemplateChild(PART_IdleGrid) as Grid;
|
||||
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.ProgrammaticNavigationCompleted += InternalFlipViewProgrammaticNavigationCompleted;
|
||||
}
|
||||
|
||||
UpdateIdleState();
|
||||
ManageCalendarOrientation();
|
||||
ManageDisplayType();
|
||||
EnsureStableSelection();
|
||||
}
|
||||
|
||||
private void InternalFlipViewProgrammaticNavigationCompleted(object? sender, ProgrammaticNavigationCompletedEventArgs e)
|
||||
{
|
||||
SelectedFlipViewDayRange = e.DayRange;
|
||||
}
|
||||
|
||||
private void UpdateIdleState()
|
||||
{
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.Opacity = IsFlipIdle ? 0 : 1;
|
||||
}
|
||||
|
||||
if (IdleGrid != null)
|
||||
{
|
||||
IdleGrid.Visibility = IsFlipIdle ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureStableSelection()
|
||||
{
|
||||
if (InternalFlipView == null || DayRanges == null || DayRanges.Count == 0)
|
||||
return;
|
||||
|
||||
var targetIndex = SelectedFlipViewIndex;
|
||||
|
||||
if (SelectedFlipViewDayRange != null)
|
||||
{
|
||||
var selectedRangeIndex = DayRanges.IndexOf(SelectedFlipViewDayRange);
|
||||
if (selectedRangeIndex >= 0)
|
||||
{
|
||||
targetIndex = selectedRangeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= DayRanges.Count)
|
||||
{
|
||||
targetIndex = 0;
|
||||
}
|
||||
|
||||
if (InternalFlipView.SelectedIndex != targetIndex)
|
||||
{
|
||||
InternalFlipView.SelectedIndex = targetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void ActiveTimelineCellUnselected(object? sender, TimelineCellUnselectedArgs e)
|
||||
=> TimelineCellUnselected?.Invoke(this, e);
|
||||
|
||||
private void ActiveTimelineCellSelected(object? sender, TimelineCellSelectedArgs e)
|
||||
=> TimelineCellSelected?.Invoke(this, e);
|
||||
|
||||
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 DispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
if (ActiveScrollViewer == null) return;
|
||||
|
||||
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;
|
||||
|
||||
ActiveCanvas.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
public void GoNextRange()
|
||||
{
|
||||
if (InternalFlipView == null) return;
|
||||
|
||||
InternalFlipView.GoNextFlip();
|
||||
}
|
||||
|
||||
public void GoPreviousRange()
|
||||
{
|
||||
if (InternalFlipView == null) return;
|
||||
|
||||
InternalFlipView.GoPreviousFlip();
|
||||
}
|
||||
|
||||
public void UnselectActiveTimelineCell()
|
||||
{
|
||||
if (ActiveCanvas == null) return;
|
||||
|
||||
ActiveCanvas.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel)
|
||||
{
|
||||
return this.FindDescendants<CalendarItemControl>().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel)!;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SizeChanged -= CalendarSizeChanged;
|
||||
|
||||
if (_previousScrollViewer != null)
|
||||
{
|
||||
DeregisterScrollChanges(_previousScrollViewer);
|
||||
_previousScrollViewer = null;
|
||||
}
|
||||
|
||||
if (_previousCanvas != null)
|
||||
{
|
||||
DeregisterCanvas(_previousCanvas);
|
||||
_previousCanvas = null;
|
||||
}
|
||||
|
||||
if (InternalFlipView != null)
|
||||
{
|
||||
InternalFlipView.ProgrammaticNavigationCompleted -= InternalFlipViewProgrammaticNavigationCompleted;
|
||||
|
||||
if (InternalFlipView is IDisposable disposableFlipView)
|
||||
{
|
||||
disposableFlipView.Dispose();
|
||||
}
|
||||
|
||||
InternalFlipView = null;
|
||||
}
|
||||
|
||||
IdleGrid = null;
|
||||
ActiveCanvas = null;
|
||||
ActiveScrollViewer = null;
|
||||
TimelineCellSelected = null;
|
||||
TimelineCellUnselected = null;
|
||||
ScrollPositionChanging = null;
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Collections;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarFlipView : CustomCalendarFlipView, IDisposable
|
||||
{
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public WinoDayTimelineCanvas? ActiveCanvas
|
||||
{
|
||||
get { return (WinoDayTimelineCanvas?)GetValue(ActiveCanvasProperty); }
|
||||
set { SetValue(ActiveCanvasProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public ScrollViewer? ActiveVerticalScrollViewer
|
||||
{
|
||||
get { return (ScrollViewer?)GetValue(ActiveVerticalScrollViewerProperty); }
|
||||
set { SetValue(ActiveVerticalScrollViewerProperty, value); }
|
||||
}
|
||||
|
||||
public bool IsIdle
|
||||
{
|
||||
get { return (bool)GetValue(IsIdleProperty); }
|
||||
set { SetValue(IsIdleProperty, value); }
|
||||
}
|
||||
|
||||
internal bool IsProgrammaticNavigationInProgress { get; private set; }
|
||||
|
||||
internal int? PendingTargetIndex { get; private set; }
|
||||
|
||||
internal event EventHandler<ProgrammaticNavigationCompletedEventArgs>? ProgrammaticNavigationCompleted;
|
||||
|
||||
private INotifyCollectionChanged? _trackedItemsSource;
|
||||
private readonly long _itemsSourceCallbackToken;
|
||||
private FrameworkElement? _pendingActiveElementContainer;
|
||||
private bool _isActiveElementResolutionPending;
|
||||
|
||||
public WinoCalendarFlipView()
|
||||
{
|
||||
_itemsSourceCallbackToken = RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged));
|
||||
}
|
||||
|
||||
private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e)
|
||||
{
|
||||
if (d is WinoCalendarFlipView flipView)
|
||||
{
|
||||
flipView.RegisterItemsSourceChange();
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterItemsSourceChange()
|
||||
{
|
||||
if (_trackedItemsSource != null)
|
||||
{
|
||||
_trackedItemsSource.CollectionChanged -= ItemsSourceUpdated;
|
||||
}
|
||||
|
||||
_trackedItemsSource = GetItemsSource();
|
||||
|
||||
if (_trackedItemsSource != null)
|
||||
{
|
||||
_trackedItemsSource.CollectionChanged += ItemsSourceUpdated;
|
||||
}
|
||||
|
||||
UpdateIdleState();
|
||||
}
|
||||
|
||||
protected override void OnSelectedItemChanged(object? oldValue, object? newValue)
|
||||
{
|
||||
base.OnSelectedItemChanged(oldValue, newValue);
|
||||
|
||||
UpdateActiveElements();
|
||||
}
|
||||
|
||||
protected override void OnContainerPrepared(DependencyObject element, object item)
|
||||
{
|
||||
base.OnContainerPrepared(element, item);
|
||||
|
||||
// Check if this is the currently selected item's container
|
||||
var index = IndexFromContainer(element);
|
||||
if (index >= 0 && index == SelectedIndex)
|
||||
{
|
||||
// Container for selected item is now ready, update active elements
|
||||
UpdateActiveElements();
|
||||
}
|
||||
}
|
||||
|
||||
private void ItemsSourceUpdated(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateIdleState();
|
||||
}
|
||||
|
||||
private void UpdateIdleState()
|
||||
{
|
||||
var itemsSource = GetItemsSource();
|
||||
IsIdle = itemsSource == null || itemsSource.Count == 0;
|
||||
}
|
||||
|
||||
private void UpdateActiveElements()
|
||||
{
|
||||
var itemsSource = GetItemsSource();
|
||||
|
||||
if (SelectedIndex < 0)
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
|
||||
if (itemsSource == null || itemsSource.Count == 0)
|
||||
{
|
||||
ActiveCanvas = null;
|
||||
ActiveVerticalScrollViewer = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryResolveActiveElements())
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
ScheduleActiveElementResolution();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveActiveElements()
|
||||
{
|
||||
if (SelectedIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ContainerFromIndex(SelectedIndex) is not FlipViewItem container)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activeCanvas = container.FindDescendant<WinoDayTimelineCanvas>();
|
||||
var activeVerticalScrollViewer = container.FindDescendant<ScrollViewer>();
|
||||
|
||||
if (activeCanvas == null && activeVerticalScrollViewer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ActiveCanvas = activeCanvas;
|
||||
ActiveVerticalScrollViewer = activeVerticalScrollViewer;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleActiveElementResolution()
|
||||
{
|
||||
if (ContainerFromIndex(SelectedIndex) is FrameworkElement container &&
|
||||
!ReferenceEquals(_pendingActiveElementContainer, container))
|
||||
{
|
||||
if (_pendingActiveElementContainer != null)
|
||||
{
|
||||
_pendingActiveElementContainer.Loaded -= PendingActiveElementContainerLoaded;
|
||||
}
|
||||
|
||||
_pendingActiveElementContainer = container;
|
||||
_pendingActiveElementContainer.Loaded += PendingActiveElementContainerLoaded;
|
||||
}
|
||||
|
||||
if (_isActiveElementResolutionPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isActiveElementResolutionPending = true;
|
||||
LayoutUpdated += FlipViewLayoutUpdated;
|
||||
|
||||
DispatcherQueue.TryEnqueue(RetryActiveElementResolution);
|
||||
}
|
||||
|
||||
private void RetryActiveElementResolution()
|
||||
{
|
||||
if (TryResolveActiveElements())
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
}
|
||||
}
|
||||
|
||||
private void PendingActiveElementContainerLoaded(object sender, RoutedEventArgs e)
|
||||
=> RetryActiveElementResolution();
|
||||
|
||||
private void FlipViewLayoutUpdated(object? sender, object e)
|
||||
=> RetryActiveElementResolution();
|
||||
|
||||
private void CancelPendingActiveElementResolution()
|
||||
{
|
||||
if (_pendingActiveElementContainer != null)
|
||||
{
|
||||
_pendingActiveElementContainer.Loaded -= PendingActiveElementContainerLoaded;
|
||||
_pendingActiveElementContainer = null;
|
||||
}
|
||||
|
||||
if (_isActiveElementResolutionPending)
|
||||
{
|
||||
LayoutUpdated -= FlipViewLayoutUpdated;
|
||||
_isActiveElementResolutionPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified date in the calendar.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Date to navigate.</param>
|
||||
public async void NavigateToDay(DateTime dateTime)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
await DispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
// Find the day range that contains the date.
|
||||
var dayRanges = GetItemsSource();
|
||||
var dayRange = dayRanges?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date));
|
||||
|
||||
if (dayRange != null && dayRanges != null)
|
||||
{
|
||||
var navigationItemIndex = dayRanges.IndexOf(dayRange);
|
||||
var hasNavigationWork = navigationItemIndex != SelectedIndex;
|
||||
|
||||
IsProgrammaticNavigationInProgress = hasNavigationWork;
|
||||
PendingTargetIndex = navigationItemIndex;
|
||||
|
||||
if (!hasNavigationWork)
|
||||
{
|
||||
PendingTargetIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Math.Abs(navigationItemIndex - SelectedIndex) > 4)
|
||||
{
|
||||
// Difference between dates are high.
|
||||
// No need to animate this much, just go without animating.
|
||||
|
||||
SelectedIndex = navigationItemIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Until we reach the day in the flip, simulate next-prev button clicks.
|
||||
// This will make sure the FlipView animations are triggered.
|
||||
// Setting SelectedIndex directly doesn't trigger the animations.
|
||||
|
||||
while (SelectedIndex != navigationItemIndex)
|
||||
{
|
||||
if (SelectedIndex > navigationItemIndex)
|
||||
{
|
||||
GoPreviousFlip();
|
||||
}
|
||||
else
|
||||
{
|
||||
GoNextFlip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SelectedIndex == PendingTargetIndex)
|
||||
{
|
||||
ProgrammaticNavigationCompleted?.Invoke(this, new ProgrammaticNavigationCompletedEventArgs(SelectedItem as DayRangeRenderModel ?? dayRange));
|
||||
}
|
||||
|
||||
IsProgrammaticNavigationInProgress = false;
|
||||
PendingTargetIndex = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ObservableRangeCollection<DayRangeRenderModel>? GetItemsSource()
|
||||
=> ItemsSource as ObservableRangeCollection<DayRangeRenderModel>;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CancelPendingActiveElementResolution();
|
||||
|
||||
if (_trackedItemsSource != null)
|
||||
{
|
||||
_trackedItemsSource.CollectionChanged -= ItemsSourceUpdated;
|
||||
_trackedItemsSource = null;
|
||||
}
|
||||
|
||||
UnregisterPropertyChangedCallback(ItemsSourceProperty, _itemsSourceCallbackToken);
|
||||
ProgrammaticNavigationCompleted = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ProgrammaticNavigationCompletedEventArgs : EventArgs
|
||||
{
|
||||
public ProgrammaticNavigationCompletedEventArgs(DayRangeRenderModel dayRange)
|
||||
{
|
||||
DayRange = dayRange;
|
||||
}
|
||||
|
||||
public DayRangeRenderModel DayRange { get; }
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Helpers;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarView : Control, IDisposable
|
||||
{
|
||||
private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder);
|
||||
private const string PART_CalendarView = nameof(PART_CalendarView);
|
||||
|
||||
public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged)));
|
||||
public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged)));
|
||||
public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(new Color()));
|
||||
|
||||
public Color TodayBackgroundColor
|
||||
{
|
||||
get { return (Color)GetValue(TodayBackgroundColorProperty); }
|
||||
set { SetValue(TodayBackgroundColorProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the command to execute when a date is picked.
|
||||
/// Unused.
|
||||
/// </summary>
|
||||
public ICommand? DateClickedCommand
|
||||
{
|
||||
get { return (ICommand?)GetValue(DateClickedCommandProperty); }
|
||||
set { SetValue(DateClickedCommandProperty, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the highlighted range of dates.
|
||||
/// </summary>
|
||||
public DateRange? HighlightedDateRange
|
||||
{
|
||||
get { return (DateRange?)GetValue(HighlightedDateRangeProperty); }
|
||||
set { SetValue(HighlightedDateRangeProperty, value); }
|
||||
}
|
||||
|
||||
public Brush? VisibleDateBackground
|
||||
{
|
||||
get { return (Brush?)GetValue(VisibleDateBackgroundProperty); }
|
||||
set { SetValue(VisibleDateBackgroundProperty, value); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
private CalendarView? CalendarView;
|
||||
private long _displayModeCallbackToken = -1;
|
||||
|
||||
public WinoCalendarView()
|
||||
{
|
||||
DefaultStyleKey = typeof(WinoCalendarView);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
if (CalendarView != null)
|
||||
{
|
||||
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
|
||||
|
||||
if (_displayModeCallbackToken != -1)
|
||||
{
|
||||
CalendarView.UnregisterPropertyChangedCallback(CalendarView.DisplayModeProperty, _displayModeCallbackToken);
|
||||
_displayModeCallbackToken = -1;
|
||||
}
|
||||
}
|
||||
|
||||
CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView;
|
||||
|
||||
Guard.IsNotNull(CalendarView, nameof(CalendarView));
|
||||
if (CalendarView == null) return;
|
||||
|
||||
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
|
||||
CalendarView.SelectedDatesChanged += InternalCalendarViewSelectionChanged;
|
||||
|
||||
// TODO: Should come from settings.
|
||||
CalendarView.FirstDayOfWeek = Windows.Globalization.DayOfWeek.Monday;
|
||||
|
||||
// Everytime display mode changes, update the visible date range backgrounds.
|
||||
// If users go back from year -> month -> day, we need to update the visible date range backgrounds.
|
||||
|
||||
_displayModeCallbackToken = CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds());
|
||||
}
|
||||
|
||||
private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
|
||||
{
|
||||
if (args.AddedDates?.Count > 0)
|
||||
{
|
||||
var clickedDate = args.AddedDates[0].Date;
|
||||
SetInnerDisplayDate(clickedDate);
|
||||
|
||||
var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
|
||||
DateClickedCommand?.Execute(clickArgs);
|
||||
}
|
||||
|
||||
// Reset selection, we don't show selected dates but react to them.
|
||||
CalendarView?.SelectedDates.Clear();
|
||||
}
|
||||
|
||||
private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoCalendarView control)
|
||||
{
|
||||
control.UpdateVisibleDateRangeBackgrounds();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime);
|
||||
|
||||
// Changing selected dates will trigger the selection changed event.
|
||||
// It will behave like user clicked the date.
|
||||
public void GoToDay(DateTime dateTime) => CalendarView?.SelectedDates.Add(dateTime);
|
||||
|
||||
private static void OnHighlightedDateRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoCalendarView control)
|
||||
{
|
||||
if (control.HighlightedDateRange == null) return;
|
||||
|
||||
control.SetInnerDisplayDate(control.HighlightedDateRange.StartDate);
|
||||
control.UpdateVisibleDateRangeBackgrounds();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateVisibleDateRangeBackgrounds()
|
||||
{
|
||||
if (HighlightedDateRange == null || VisibleDateBackground == null || CalendarView == null) return;
|
||||
|
||||
var markDateCalendarDayItems = WinoVisualTreeHelper.FindDescendants<CalendarViewDayItem>(CalendarView);
|
||||
|
||||
foreach (var calendarDayItem in markDateCalendarDayItems)
|
||||
{
|
||||
var border = WinoVisualTreeHelper.GetChildObject<Border>(calendarDayItem, PART_DayViewItemBorder);
|
||||
|
||||
if (border == null) return;
|
||||
|
||||
if (calendarDayItem.Date.Date == DateTime.Today.Date)
|
||||
{
|
||||
border.Background = new SolidColorBrush(TodayBackgroundColor);
|
||||
}
|
||||
else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date)
|
||||
{
|
||||
border.Background = VisibleDateBackground;
|
||||
}
|
||||
else
|
||||
{
|
||||
border.Background = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (CalendarView == null)
|
||||
return;
|
||||
|
||||
CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged;
|
||||
|
||||
if (_displayModeCallbackToken != -1)
|
||||
{
|
||||
CalendarView.UnregisterPropertyChangedCallback(CalendarView.DisplayModeProperty, _displayModeCallbackToken);
|
||||
_displayModeCallbackToken = -1;
|
||||
}
|
||||
|
||||
CalendarView = null;
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Windows;
|
||||
using Windows.Foundation;
|
||||
using Wino.Calendar.Args;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoDayTimelineCanvas : Control, IDisposable
|
||||
{
|
||||
public event EventHandler<TimelineCellSelectedArgs>? TimelineCellSelected;
|
||||
public event EventHandler<TimelineCellUnselectedArgs>? TimelineCellUnselected;
|
||||
|
||||
private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
|
||||
private SKXamlCanvas? Canvas;
|
||||
|
||||
public static readonly DependencyProperty RenderOptionsProperty = DependencyProperty.Register(nameof(RenderOptions), typeof(CalendarRenderOptions), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty SeperatorColorProperty = DependencyProperty.Register(nameof(SeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty SelectedCellBackgroundBrushProperty = DependencyProperty.Register(nameof(SelectedCellBackgroundBrush), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty WorkingHourCellBackgroundColorProperty = DependencyProperty.Register(nameof(WorkingHourCellBackgroundColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
|
||||
public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register(nameof(SelectedDateTime), typeof(DateTime?), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedDateTimeChanged)));
|
||||
public static readonly DependencyProperty PositionerUIElementProperty = DependencyProperty.Register(nameof(PositionerUIElement), typeof(UIElement), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null));
|
||||
|
||||
public UIElement? PositionerUIElement
|
||||
{
|
||||
get { return (UIElement?)GetValue(PositionerUIElementProperty); }
|
||||
set { SetValue(PositionerUIElementProperty, value); }
|
||||
}
|
||||
|
||||
public CalendarRenderOptions? RenderOptions
|
||||
{
|
||||
get { return (CalendarRenderOptions?)GetValue(RenderOptionsProperty); }
|
||||
set { SetValue(RenderOptionsProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? HalfHourSeperatorColor
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(HalfHourSeperatorColorProperty); }
|
||||
set { SetValue(HalfHourSeperatorColorProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? SeperatorColor
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(SeperatorColorProperty); }
|
||||
set { SetValue(SeperatorColorProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? WorkingHourCellBackgroundColor
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(WorkingHourCellBackgroundColorProperty); }
|
||||
set { SetValue(WorkingHourCellBackgroundColorProperty, value); }
|
||||
}
|
||||
|
||||
public SolidColorBrush? SelectedCellBackgroundBrush
|
||||
{
|
||||
get { return (SolidColorBrush?)GetValue(SelectedCellBackgroundBrushProperty); }
|
||||
set { SetValue(SelectedCellBackgroundBrushProperty, value); }
|
||||
}
|
||||
|
||||
public DateTime? SelectedDateTime
|
||||
{
|
||||
get { return (DateTime?)GetValue(SelectedDateTimeProperty); }
|
||||
set { SetValue(SelectedDateTimeProperty, value); }
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
Canvas = GetTemplateChild(PART_InternalCanvas) as SKXamlCanvas;
|
||||
|
||||
if (Canvas != null)
|
||||
{
|
||||
Canvas.PaintSurface += OnCanvasPaintSurface;
|
||||
Canvas.PointerPressed += OnCanvasPointerPressed;
|
||||
}
|
||||
|
||||
ForceDraw();
|
||||
}
|
||||
|
||||
private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoDayTimelineCanvas control)
|
||||
{
|
||||
if (e.OldValue != null && e.NewValue == null)
|
||||
{
|
||||
control.RaiseCellUnselected();
|
||||
}
|
||||
|
||||
control.ForceDraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseCellUnselected()
|
||||
{
|
||||
TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
|
||||
}
|
||||
|
||||
private void OnCanvasPointerPressed(object? sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (RenderOptions == null) return;
|
||||
var canvas = Canvas;
|
||||
if (canvas == null) return;
|
||||
|
||||
var hourHeight = RenderOptions.CalendarSettings.HourHeight;
|
||||
|
||||
// When users click to cell we need to find the day, hour and minutes (first 30 minutes or second 30 minutes) that it represents on the timeline.
|
||||
|
||||
if (PositionerUIElement == null)
|
||||
{
|
||||
PositionerUIElement = this.FindParents().LastOrDefault(a => a is Grid);
|
||||
}
|
||||
|
||||
if (PositionerUIElement == null)
|
||||
return;
|
||||
|
||||
PointerPoint positionerRootPoint = e.GetCurrentPoint(PositionerUIElement);
|
||||
PointerPoint canvasPointerPoint = e.GetCurrentPoint(canvas);
|
||||
|
||||
Point touchPoint = canvasPointerPoint.Position;
|
||||
|
||||
var singleDayWidth = (canvas.ActualWidth / RenderOptions.TotalDayCount);
|
||||
|
||||
int day = (int)(touchPoint.X / singleDayWidth);
|
||||
int hour = (int)(touchPoint.Y / hourHeight);
|
||||
|
||||
bool isSecondHalf = touchPoint.Y % hourHeight > (hourHeight / 2);
|
||||
|
||||
var diffX = positionerRootPoint.Position.X - touchPoint.X;
|
||||
var diffY = positionerRootPoint.Position.Y - touchPoint.Y;
|
||||
|
||||
var cellStartRelativePositionX = diffX + (day * singleDayWidth);
|
||||
var cellEndRelativePositionX = cellStartRelativePositionX + singleDayWidth;
|
||||
|
||||
var cellStartRelativePositionY = diffY + (hour * hourHeight) + (isSecondHalf ? hourHeight / 2 : 0);
|
||||
var cellEndRelativePositionY = cellStartRelativePositionY + (isSecondHalf ? (hourHeight / 2) : hourHeight);
|
||||
|
||||
var cellSize = new Size(cellEndRelativePositionX - cellStartRelativePositionX, hourHeight / 2);
|
||||
var positionerPoint = new Point(cellStartRelativePositionX, cellStartRelativePositionY);
|
||||
|
||||
var clickedDateTime = RenderOptions.DateRange.StartDate.AddDays(day).AddHours(hour).AddMinutes(isSecondHalf ? 30 : 0);
|
||||
|
||||
// If there is already a selected date, in order to mimic the popup behavior, we need to dismiss the previous selection first.
|
||||
// Next click will be a new selection.
|
||||
|
||||
// Raise the events directly here instead of DP to not lose pointer position.
|
||||
if (clickedDateTime == SelectedDateTime || SelectedDateTime != null)
|
||||
{
|
||||
SelectedDateTime = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedDateTime = clickedDateTime;
|
||||
TimelineCellSelected?.Invoke(this, new TimelineCellSelectedArgs(clickedDateTime, touchPoint, positionerPoint, cellSize));
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Clicked: {clickedDateTime}");
|
||||
}
|
||||
|
||||
public WinoDayTimelineCanvas()
|
||||
{
|
||||
DefaultStyleKey = typeof(WinoDayTimelineCanvas);
|
||||
}
|
||||
|
||||
private static void OnRenderingPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinoDayTimelineCanvas control)
|
||||
{
|
||||
control.ForceDraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceDraw() => Canvas?.Invalidate();
|
||||
|
||||
private bool CanDrawTimeline()
|
||||
{
|
||||
return RenderOptions != null
|
||||
&& Canvas != null
|
||||
&& WorkingHourCellBackgroundColor != null
|
||||
&& SeperatorColor != null
|
||||
&& HalfHourSeperatorColor != null
|
||||
&& SelectedCellBackgroundBrush != null;
|
||||
}
|
||||
|
||||
private void OnCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
||||
{
|
||||
if (!CanDrawTimeline()) return;
|
||||
var renderOptions = RenderOptions!;
|
||||
var workingHourCellBackgroundColor = WorkingHourCellBackgroundColor!;
|
||||
var seperatorColor = SeperatorColor!;
|
||||
var halfHourSeperatorColor = HalfHourSeperatorColor!;
|
||||
var selectedCellBackgroundBrush = SelectedCellBackgroundBrush!;
|
||||
|
||||
var canvas = e.Surface.Canvas;
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
int hours = 24;
|
||||
|
||||
double canvasWidth = e.Info.Width;
|
||||
double canvasHeight = e.Info.Height;
|
||||
|
||||
if (canvasWidth == 0 || canvasHeight == 0) return;
|
||||
|
||||
// Calculate the width of each rectangle (1 day column)
|
||||
// Equal distribution of the whole width.
|
||||
double rectWidth = canvasWidth / renderOptions.TotalDayCount;
|
||||
|
||||
// Calculate the height of each rectangle (1 hour row)
|
||||
double rectHeight = renderOptions.CalendarSettings.HourHeight;
|
||||
|
||||
// Define stroke and fill colors
|
||||
var strokeColor = ToSKColor(seperatorColor.Color);
|
||||
float strokeThickness = 0.5f;
|
||||
|
||||
// Create paints for drawing
|
||||
using var strokePaint = new SKPaint
|
||||
{
|
||||
Color = strokeColor,
|
||||
StrokeWidth = strokeThickness,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
using var fillPaint = new SKPaint
|
||||
{
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
using var dashedPaint = new SKPaint
|
||||
{
|
||||
Color = ToSKColor(halfHourSeperatorColor.Color),
|
||||
StrokeWidth = strokeThickness,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
PathEffect = SKPathEffect.CreateDash([2f, 2f], 0),
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
for (int day = 0; day < renderOptions.TotalDayCount; day++)
|
||||
{
|
||||
var currentDay = renderOptions.DateRange.StartDate.AddDays(day);
|
||||
|
||||
bool isWorkingDay = renderOptions.CalendarSettings.WorkingDays.Contains(currentDay.DayOfWeek);
|
||||
|
||||
// Loop through each hour (rows)
|
||||
for (int hour = 0; hour < hours; hour++)
|
||||
{
|
||||
var renderTime = TimeSpan.FromHours(hour);
|
||||
|
||||
var representingDateTime = currentDay.AddHours(hour);
|
||||
|
||||
// Calculate the position and size of the rectangle
|
||||
float x = (float)(day * rectWidth);
|
||||
float y = (float)(hour * rectHeight);
|
||||
float width = (float)rectWidth;
|
||||
float height = (float)rectHeight;
|
||||
|
||||
var rectangle = new SKRect(x, y, x + width, y + height);
|
||||
|
||||
// Draw the rectangle border.
|
||||
// This is the main rectangle.
|
||||
canvas.DrawRect(rectangle, strokePaint);
|
||||
|
||||
// Fill another rectangle with the working hour background color
|
||||
// This rectangle must be placed with -1 margin to prevent invisible borders of the main rectangle.
|
||||
if (isWorkingDay && renderTime >= renderOptions.CalendarSettings.WorkingHourStart && renderTime <= renderOptions.CalendarSettings.WorkingHourEnd)
|
||||
{
|
||||
var backgroundRectangle = new SKRect(x + 1, y + 1, x + width - 1, y + height - 1);
|
||||
|
||||
canvas.DrawRect(backgroundRectangle, strokePaint);
|
||||
fillPaint.Color = ToSKColor(workingHourCellBackgroundColor.Color);
|
||||
canvas.DrawRect(backgroundRectangle, fillPaint);
|
||||
}
|
||||
|
||||
// Draw a line in the center of the rectangle for representing half hours.
|
||||
float lineY = y + height / 2;
|
||||
canvas.DrawLine(x, lineY, x + width, lineY, dashedPaint);
|
||||
}
|
||||
|
||||
// Draw selected item background color for the date if possible.
|
||||
if (SelectedDateTime != null)
|
||||
{
|
||||
var selectedDateTime = SelectedDateTime.Value;
|
||||
if (selectedDateTime.Date == currentDay.Date)
|
||||
{
|
||||
var selectionRectHeight = rectHeight / 2;
|
||||
var selectedY = selectedDateTime.Hour * rectHeight + (selectedDateTime.Minute / 60) * rectHeight;
|
||||
|
||||
// Second half of the hour is selected.
|
||||
if (selectedDateTime.TimeOfDay.Minutes == 30)
|
||||
{
|
||||
selectedY += rectHeight / 2;
|
||||
}
|
||||
|
||||
var selectedRectangle = new SKRect(
|
||||
(float)(day * rectWidth),
|
||||
(float)selectedY,
|
||||
(float)(day * rectWidth + rectWidth),
|
||||
(float)(selectedY + selectionRectHeight));
|
||||
|
||||
fillPaint.Color = ToSKColor(selectedCellBackgroundBrush.Color);
|
||||
canvas.DrawRect(selectedRectangle, fillPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SKColor ToSKColor(Windows.UI.Color color)
|
||||
{
|
||||
return new SKColor(color.R, color.G, color.B, color.A);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Canvas == null) return;
|
||||
|
||||
Canvas.PaintSurface -= OnCanvasPaintSurface;
|
||||
Canvas.PointerPressed -= OnCanvasPointerPressed;
|
||||
|
||||
Canvas = null;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
x:Class="Wino.Mail.WinUI.Controls.UpdateNotesFlipViewControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:Wino.Core.Domain.Models.Updates"
|
||||
@@ -22,14 +21,21 @@
|
||||
<DataTemplate x:DataType="models:UpdateNoteSection">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Padding="8" Spacing="12">
|
||||
<controls:MarkdownTextBlock HorizontalAlignment="Center" Text="{x:Bind Title, Mode=OneTime}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Text="{x:Bind Title, Mode=OneTime}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<Image
|
||||
Width="{x:Bind ActualImageWidth, Mode=OneTime}"
|
||||
Height="{x:Bind ActualImageHeight, Mode=OneTime}"
|
||||
HorizontalAlignment="Center"
|
||||
Source="{x:Bind ImageUrl, Mode=OneTime}"
|
||||
Stretch="Uniform" />
|
||||
<controls:MarkdownTextBlock HorizontalAlignment="Stretch" Text="{x:Bind Description, Mode=OneTime}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{x:Bind Description, Mode=OneTime}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -474,17 +474,14 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
return true;
|
||||
}
|
||||
|
||||
private static LoadCalendarMessage CreateLoadCalendarMessage(CalendarPageNavigationArgs args)
|
||||
private LoadCalendarMessage CreateLoadCalendarMessage(CalendarPageNavigationArgs args)
|
||||
{
|
||||
var targetDate = args.RequestDefaultNavigation
|
||||
? DateTime.Now.Date
|
||||
: args.NavigationDate;
|
||||
? DateOnly.FromDateTime(DateTime.Now.Date)
|
||||
: DateOnly.FromDateTime(args.NavigationDate.Date);
|
||||
|
||||
var initiative = args.RequestDefaultNavigation
|
||||
? CalendarInitInitiative.App
|
||||
: CalendarInitInitiative.User;
|
||||
|
||||
return new LoadCalendarMessage(targetDate, initiative);
|
||||
var displayRequest = new CalendarDisplayRequest(_statePersistanceService.CalendarDisplayType, targetDate);
|
||||
return new LoadCalendarMessage(displayRequest);
|
||||
}
|
||||
|
||||
private bool NavigateInnerShellFrame(Frame frame, Type pageType, object? parameter, NavigationTransitionType transition)
|
||||
|
||||
@@ -396,6 +396,8 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
||||
|
||||
return new CalendarSettings(FirstDayOfWeek,
|
||||
workingDays,
|
||||
WorkingDayStart,
|
||||
WorkingDayEnd,
|
||||
WorkingHourStart,
|
||||
WorkingHourEnd,
|
||||
HourHeight,
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
<ResourceDictionary
|
||||
x:Class="Wino.Styles.WinoCalendarResources"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:calendar="using:Wino.Mail.WinUI.Controls.Calendar"
|
||||
xmlns:controls="using:Wino.Calendar.Controls"
|
||||
xmlns:controls2="using:Wino.Mail.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Wino.Calendar.ViewModels.Data"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
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:selectors1="using:Wino.Selectors"
|
||||
xmlns:toolkitControls="using:CommunityToolkit.WinUI.Controls">
|
||||
|
||||
<!-- 08:00 or 8 AM/PM on the left etc. -->
|
||||
<DataTemplate x:Key="DayCalendarHourHeaderTemplate" x:DataType="models:DayHeaderRenderModel">
|
||||
<Grid Height="{x:Bind HourHeight}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Text="{x:Bind DayHeader}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- ShowAs Status Stripe Templates -->
|
||||
<DataTemplate x:Key="FreeStripeTemplate" x:DataType="data:CalendarItemViewModel">
|
||||
<Border Background="#4CAF50" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="TentativeStripeTemplate" x:DataType="data:CalendarItemViewModel">
|
||||
<Border Background="#FFC107" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="BusyStripeTemplate" x:DataType="data:CalendarItemViewModel">
|
||||
<Border Background="#ff7675" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="OutOfOfficeStripeTemplate" x:DataType="data:CalendarItemViewModel">
|
||||
<Border Background="#9C27B0" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="WorkingElsewhereStripeTemplate" x:DataType="data:CalendarItemViewModel">
|
||||
<Border Background="#2196F3" />
|
||||
</DataTemplate>
|
||||
|
||||
<!-- ShowAs Status Stripe Selector -->
|
||||
<selectors1:CalendarItemShowAsStripeTemplateSelector
|
||||
x:Key="ShowAsStripeSelector"
|
||||
BusyTemplate="{StaticResource BusyStripeTemplate}"
|
||||
FreeTemplate="{StaticResource FreeStripeTemplate}"
|
||||
OutOfOfficeTemplate="{StaticResource OutOfOfficeStripeTemplate}"
|
||||
TentativeTemplate="{StaticResource TentativeStripeTemplate}"
|
||||
WorkingElsewhereTemplate="{StaticResource WorkingElsewhereStripeTemplate}" />
|
||||
|
||||
<!-- Vertical panel that renders items on canvas. -->
|
||||
<DataTemplate x:Key="DayCalendarItemVerticalRenderTemplate" x:DataType="models:CalendarDayModel">
|
||||
<ItemsControl
|
||||
x:Name="RegularEventItemsControl"
|
||||
ItemsSource="{x:Bind EventsCollection.RegularEvents}"
|
||||
Loaded="OnRegularEventItemsControlLoaded">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<!-- Default Calendar Item View Model Template -->
|
||||
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||
<controls:CalendarItemControl CalendarItem="{x:Bind}" IsCustomEventArea="False" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<PaneThemeTransition Edge="Left" />
|
||||
</TransitionCollection>
|
||||
</ItemsControl.ItemContainerTransitions>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:WinoCalendarPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Equally distributed days of week representation in FlipView. -->
|
||||
<!-- Used for day-week-work week templates. -->
|
||||
<!-- Horizontal template -->
|
||||
<DataTemplate x:Key="FlipTemplate" x:DataType="models:DayRangeRenderModel">
|
||||
<Grid
|
||||
Background="Transparent"
|
||||
ColumnSpacing="0"
|
||||
RowSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" MinHeight="100" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ItemsControl
|
||||
x:Name="DayColumnsItemsControl"
|
||||
Margin="50,0,16,0"
|
||||
ItemsSource="{x:Bind CalendarDays}"
|
||||
Loaded="OnDayColumnsItemsControlLoaded">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:CalendarDayModel">
|
||||
<controls:DayColumnControl DayModel="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<toolkitControls:UniformGrid Orientation="Horizontal" Rows="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0"
|
||||
Padding="0,0,16,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Rendering left hour headers. -->
|
||||
<ItemsControl ItemTemplate="{StaticResource DayCalendarHourHeaderTemplate}" ItemsSource="{x:Bind DayHeaders}" />
|
||||
|
||||
<!-- Drawing canvas for timeline. -->
|
||||
<controls:WinoDayTimelineCanvas
|
||||
Grid.Column="1"
|
||||
HalfHourSeperatorColor="{ThemeResource CalendarSeperatorBrush}"
|
||||
RenderOptions="{x:Bind CalendarRenderOptions}"
|
||||
SelectedCellBackgroundBrush="{ThemeResource CalendarFieldSelectedBackgroundBrush}"
|
||||
SeperatorColor="{ThemeResource CalendarSeperatorBrush}"
|
||||
WorkingHourCellBackgroundColor="{ThemeResource CalendarFieldWorkingHoursBackgroundBrush}" />
|
||||
|
||||
<!-- Each vertical day grids that renders events. -->
|
||||
<ItemsControl
|
||||
x:Name="EventGridsItemsControl"
|
||||
Grid.Column="1"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
ItemTemplate="{StaticResource DayCalendarItemVerticalRenderTemplate}"
|
||||
ItemsSource="{x:Bind CalendarDays}"
|
||||
Loaded="OnEventGridsItemsControlLoaded">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<toolkitControls:UniformGrid Orientation="Horizontal" Rows="1" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template that displays 35 days in total. -->
|
||||
<!-- Used for monthly view -->
|
||||
<!-- Vertical template -->
|
||||
<DataTemplate x:Key="MonthlyFlipTemplate" x:DataType="models:DayRangeRenderModel">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Height="20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
</Grid>
|
||||
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind CalendarDays}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:CalendarDayModel">
|
||||
<controls:DayColumnControl DayModel="{x:Bind Mode=OneWay}" Template="{StaticResource MonthlyColumnControlTemplate}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls2:EqualGridPanel Columns="7" Rows="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<ItemsPanelTemplate x:Key="VerticalFlipViewItemsPanel">
|
||||
<VirtualizingStackPanel Orientation="Vertical" />
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
<ItemsPanelTemplate x:Key="HorizontalFlipViewItemsPanel">
|
||||
<VirtualizingStackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
|
||||
|
||||
<!-- Default style for WinoCalendarControl -->
|
||||
<Style TargetType="controls:WinoCalendarControl">
|
||||
<Style.Setters>
|
||||
<Setter Property="HorizontalItemsPanelTemplate" Value="{StaticResource HorizontalFlipViewItemsPanel}" />
|
||||
<Setter Property="VerticalItemsPanelTemplate" Value="{StaticResource VerticalFlipViewItemsPanel}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:WinoCalendarControl">
|
||||
<Grid>
|
||||
<controls:WinoCalendarFlipView
|
||||
x:Name="PART_WinoFlipView"
|
||||
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"
|
||||
ItemsSource="{TemplateBinding DayRanges}"
|
||||
SelectedIndex="{Binding SelectedFlipViewIndex, RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay}">
|
||||
<controls:WinoCalendarFlipView.ItemTemplateSelector>
|
||||
<selectors1:WinoCalendarItemTemplateSelector
|
||||
DayWeekWorkWeekTemplate="{StaticResource FlipTemplate}"
|
||||
DisplayType="{x:Bind DisplayType, Mode=OneWay}"
|
||||
MonthlyTemplate="{StaticResource MonthlyFlipTemplate}" />
|
||||
</controls:WinoCalendarFlipView.ItemTemplateSelector>
|
||||
</controls:WinoCalendarFlipView>
|
||||
|
||||
<Grid x:Name="PART_IdleGrid" Visibility="Collapsed">
|
||||
<muxc:ProgressRing
|
||||
Width="50"
|
||||
Height="50"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="True" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Top header control for days. -->
|
||||
<ControlTemplate x:Key="DailyColumnControlTemplate" TargetType="controls:DayColumnControl">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="25" />
|
||||
<RowDefinition Height="7" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Name of the day. Monday, Tuesday etc. at the top. -->
|
||||
<TextBlock
|
||||
x:Name="PART_ColumnHeaderText"
|
||||
Margin="8,0,0,0"
|
||||
FontSize="16"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Grid.RowSpan="2"
|
||||
BorderBrush="{ThemeResource CalendarSeperatorBrush}"
|
||||
BorderThickness="1,1,0,1" />
|
||||
|
||||
<!-- Border for today indication. -->
|
||||
<Border
|
||||
x:Name="PART_IsTodayBorder"
|
||||
Grid.Row="1"
|
||||
Height="5"
|
||||
Margin="2,0,2,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SystemAccentColor}"
|
||||
CornerRadius="2"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<!-- Place where full day events go. -->
|
||||
<Grid
|
||||
x:Name="PART_DayDataAreaGrid"
|
||||
Grid.Row="2"
|
||||
Padding="6"
|
||||
BorderBrush="{ThemeResource CalendarSeperatorBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="35" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Day number -->
|
||||
<TextBlock x:Name="PART_HeaderDateDayText" FontSize="17" />
|
||||
|
||||
<!-- Extras -->
|
||||
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
|
||||
|
||||
<!-- All-Multi Day Events -->
|
||||
<ItemsControl
|
||||
x:Name="PART_AllDayItemsControl"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,6">
|
||||
<ItemsControl.ItemTemplateSelector>
|
||||
<selectors:CustomAreaCalendarItemSelector>
|
||||
<selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
|
||||
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||
<controls:CalendarItemControl CalendarItem="{x:Bind}" IsCustomEventArea="True" />
|
||||
</DataTemplate>
|
||||
</selectors:CustomAreaCalendarItemSelector.AllDayTemplate>
|
||||
<selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
|
||||
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||
<controls:CalendarItemControl CalendarItem="{x:Bind}" IsCustomEventArea="True" />
|
||||
</DataTemplate>
|
||||
</selectors:CustomAreaCalendarItemSelector.MultiDayTemplate>
|
||||
</selectors:CustomAreaCalendarItemSelector>
|
||||
</ItemsControl.ItemTemplateSelector>
|
||||
<ItemsControl.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<AddDeleteThemeTransition />
|
||||
</TransitionCollection>
|
||||
</ItemsControl.ItemContainerTransitions>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical" Spacing="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="TodayOrNotStates">
|
||||
<VisualState x:Name="NotTodayState" />
|
||||
<VisualState x:Name="TodayState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_IsTodayBorder.Visibility" Value="Visible" />
|
||||
<Setter Target="PART_HeaderDateDayText.Foreground" Value="{ThemeResource SystemAccentColor}" />
|
||||
<Setter Target="PART_HeaderDateDayText.FontWeight" Value="Semibold" />
|
||||
<Setter Target="PART_ColumnHeaderText.FontWeight" Value="Semibold" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
<!-- Monthly data control for months -->
|
||||
<ControlTemplate x:Key="MonthlyColumnControlTemplate" TargetType="controls:DayColumnControl">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="5" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Border for today indication. -->
|
||||
<Border
|
||||
x:Name="PART_IsTodayBorder"
|
||||
Grid.Row="0"
|
||||
Height="5"
|
||||
Margin="2,0,2,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SystemAccentColor}"
|
||||
CornerRadius="2"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<!-- Border -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
BorderBrush="{ThemeResource CalendarSeperatorBrush}"
|
||||
BorderThickness="1,1,0,1" />
|
||||
|
||||
<!-- Place where full day events go. -->
|
||||
<Grid
|
||||
x:Name="PART_DayDataAreaGrid"
|
||||
Grid.Row="1"
|
||||
Padding="6"
|
||||
BorderBrush="{ThemeResource CalendarSeperatorBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="35" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Day number -->
|
||||
<TextBlock x:Name="PART_HeaderDateDayText" FontSize="17" />
|
||||
|
||||
<!-- Extras -->
|
||||
<StackPanel Grid.Column="1" HorizontalAlignment="Right" />
|
||||
|
||||
<!-- All events summary. -->
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,6"
|
||||
Padding="0,0,16,0">
|
||||
<ItemsControl x:Name="PART_AllDayItemsControl">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:CalendarItemViewModel">
|
||||
<controls:CalendarItemControl CalendarItem="{x:Bind}" IsCustomEventArea="True" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<AddDeleteThemeTransition />
|
||||
</TransitionCollection>
|
||||
</ItemsControl.ItemContainerTransitions>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical" Spacing="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="TodayOrNotStates">
|
||||
<VisualState x:Name="NotTodayState" />
|
||||
<VisualState x:Name="TodayState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_IsTodayBorder.Visibility" Value="Visible" />
|
||||
<Setter Target="PART_HeaderDateDayText.Foreground" Value="{ThemeResource SystemAccentColor}" />
|
||||
<Setter Target="PART_HeaderDateDayText.FontWeight" Value="Semibold" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
|
||||
<!-- Default style for DayColumnControl -->
|
||||
<Style TargetType="controls:DayColumnControl">
|
||||
<Setter Property="Template" Value="{StaticResource DailyColumnControlTemplate}" />
|
||||
</Style>
|
||||
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Calendar.Controls;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Styles;
|
||||
|
||||
public sealed partial class WinoCalendarResources : ResourceDictionary
|
||||
{
|
||||
public WinoCalendarResources()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnRegularEventItemsControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ItemsControl itemsControl && itemsControl.DataContext is CalendarDayModel dayModel)
|
||||
{
|
||||
if (itemsControl.ItemsPanelRoot is WinoCalendarPanel panel)
|
||||
{
|
||||
panel.HourHeight = dayModel.CalendarRenderOptions.CalendarSettings.HourHeight;
|
||||
panel.Period = dayModel.Period;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDayColumnsItemsControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ItemsControl itemsControl && itemsControl.DataContext is DayRangeRenderModel rangeModel)
|
||||
{
|
||||
if (itemsControl.ItemsPanelRoot is UniformGrid uniformGrid)
|
||||
{
|
||||
uniformGrid.Columns = rangeModel.CalendarRenderOptions.TotalDayCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEventGridsItemsControlLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ItemsControl itemsControl && itemsControl.DataContext is DayRangeRenderModel rangeModel)
|
||||
{
|
||||
if (itemsControl.ItemsPanelRoot is UniformGrid uniformGrid)
|
||||
{
|
||||
uniformGrid.Columns = rangeModel.CalendarRenderOptions.TotalDayCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Wino.Calendar.Controls">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="CalendarThemeResources.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style TargetType="controls:WinoCalendarView">
|
||||
<Setter Property="VisibleDateBackground" Value="{ThemeResource WinoCalendarViewVisibleDayBackgroundBrush}" />
|
||||
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:WinoCalendarView">
|
||||
<CalendarView
|
||||
x:Name="PART_CalendarView"
|
||||
BorderBrush="{ThemeResource WinoCalendarViewBorderBrush}"
|
||||
CalendarItemCornerRadius="5"
|
||||
CornerRadius="4"
|
||||
DayItemMargin="0"
|
||||
IsTodayHighlighted="False"
|
||||
SelectionMode="Single">
|
||||
<CalendarView.CalendarViewDayItemStyle>
|
||||
<Style TargetType="CalendarViewDayItem">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CalendarViewDayItem">
|
||||
<Grid>
|
||||
<Border
|
||||
x:Name="PART_DayViewItemBorder"
|
||||
Margin="0,-1,0,-3"
|
||||
CornerRadius="5">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</CalendarView.CalendarViewDayItemStyle>
|
||||
</CalendarView>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,19 +0,0 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Wino.Calendar.Controls"
|
||||
xmlns:skia="using:SkiaSharp.Views.Windows">
|
||||
|
||||
<!-- Background Timeline Canvas -->
|
||||
<Style TargetType="controls:WinoDayTimelineCanvas">
|
||||
<Style.Setters>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:WinoDayTimelineCanvas">
|
||||
<skia:SKXamlCanvas x:Name="PART_InternalCanvas" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -131,13 +131,26 @@
|
||||
Text="{x:Bind domain:Translator.CalendarEventCompose_NewEventButton, Mode=OneTime}" />
|
||||
</coreControls:WinoNavigationViewItem>
|
||||
|
||||
<calendarControls:WinoCalendarView
|
||||
x:Name="CalendarView"
|
||||
<Border
|
||||
x:Name="RangeSummaryCard"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
DateClickedCommand="{x:Bind ViewModel.DateClickedCommand}"
|
||||
HighlightedDateRange="{x:Bind ViewModel.HighlightedDateRange, Mode=OneWay}"
|
||||
TodayBackgroundColor="{ThemeResource SystemAccentColor}" />
|
||||
Margin="16,0,16,12"
|
||||
Padding="12"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.VisibleDateRangeText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
|
||||
<muxc:CalendarView
|
||||
x:Name="VisibleDateRangeCalendarView"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionMode="Multiple"
|
||||
SelectedDatesChanged="VisibleDateRangeCalendarViewSelectedDatesChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Account Calendars Host -->
|
||||
<ListView
|
||||
@@ -261,8 +274,6 @@
|
||||
</VisualState>
|
||||
<VisualState x:Name="SmallScreen">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="NavigationTitleStack.Visibility" Value="Collapsed" />
|
||||
<Setter Target="SearchBox.(Grid.ColumnSpan)" Value="2" />
|
||||
<Setter Target="navigationView.IsPaneOpen" Value="False" />
|
||||
</VisualState.Setters>
|
||||
<VisualState.StateTriggers>
|
||||
@@ -273,28 +284,14 @@
|
||||
|
||||
<VisualStateGroup x:Name="CalendarOrientationStates">
|
||||
<VisualState x:Name="HorizontalCalendar" />
|
||||
<VisualState x:Name="VerticalCalendar">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="DayHeaderNavigationItemsFlipView.ItemsPanel">
|
||||
<Setter.Value>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel Orientation="Vertical" />
|
||||
</ItemsPanelTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Target="PreviousDateButtonPathIcon.Data" Value="F1 M 3.2 10.52 C 2.986666 10.733333 2.92 10.98 3 11.259999 C 3.08 11.54 3.266666 11.713333 3.56 11.78 C 3.853333 11.846666 4.093333 11.773333 4.28 11.559999 L 9.24 6.32 L 9.24 19.039999 C 9.24 19.253332 9.313333 19.433332 9.46 19.58 C 9.606666 19.726665 9.786666 19.799999 10 19.799999 C 10.213333 19.799999 10.393332 19.726665 10.54 19.58 C 10.686666 19.433332 10.76 19.253332 10.76 19.039999 L 10.76 6.32 L 15.719999 11.559999 C 15.906666 11.773333 16.139999 11.846666 16.42 11.78 C 16.700001 11.713333 16.886665 11.54 16.98 11.259999 C 17.073332 10.98 17.013332 10.733333 16.799999 10.52 L 10.719999 4.119999 C 10.559999 3.959999 10.373333 3.853333 10.16 3.799999 C 10.053333 3.799999 9.946667 3.799999 9.84 3.799999 C 9.626666 3.853333 9.439999 3.959999 9.28 4.119999 Z " />
|
||||
<Setter Target="NextDateButtonPathIcon.Data" Value="F1 M 16.799999 13.079999 C 16.933332 12.92 16.993332 12.74 16.98 12.539999 C 16.966665 12.34 16.886665 12.166667 16.74 12.02 C 16.593334 11.873333 16.42 11.799999 16.219999 11.799999 C 16.02 11.799999 15.853333 11.879999 15.719999 12.039999 L 10.76 17.279999 L 10.76 4.559999 C 10.76 4.346666 10.686666 4.166668 10.54 4.02 C 10.393332 3.873333 10.213333 3.799999 10 3.799999 C 9.786666 3.799999 9.606666 3.873333 9.46 4.02 C 9.313333 4.166668 9.24 4.346666 9.24 4.559999 L 9.24 17.279999 L 4.28 12.039999 C 4.146667 11.879999 3.98 11.799999 3.78 11.799999 C 3.58 11.799999 3.4 11.873333 3.24 12.02 C 3.08 12.166667 3 12.34 3 12.539999 C 3 12.74 3.066667 12.92 3.2 13.079999 L 9.28 19.48 C 9.439999 19.639999 9.626666 19.746666 9.84 19.799999 C 9.946667 19.799999 10.053333 19.799999 10.16 19.799999 C 10.373333 19.746666 10.559999 19.639999 10.719999 19.48 Z " />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="VerticalCalendar" />
|
||||
</VisualStateGroup>
|
||||
|
||||
<VisualStateGroup x:Name="ShellStateContentGroup">
|
||||
<VisualState x:Name="DefaultShellContentState" />
|
||||
<VisualState x:Name="EventDetailsContentState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ShellContentArea.Visibility" Value="Collapsed" />
|
||||
<Setter Target="CalendarTypeSelector.Visibility" Value="Collapsed" />
|
||||
<Setter Target="CalendarView.IsEnabled" Value="False" />
|
||||
<Setter Target="RangeSummaryCard.IsEnabled" Value="False" />
|
||||
<Setter Target="CalendarHostListView.IsEnabled" Value="False" />
|
||||
</VisualState.Setters>
|
||||
<VisualState.StateTriggers>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -11,6 +12,7 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.MenuItems;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Mail.Views.Abstract;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Windows.System;
|
||||
@@ -22,6 +24,7 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
{
|
||||
private const string STATE_HorizontalCalendar = "HorizontalCalendar";
|
||||
private const string STATE_VerticalCalendar = "VerticalCalendar";
|
||||
private bool _isSynchronizingVisibleDateRangeCalendar;
|
||||
|
||||
public Frame GetShellFrame() => InnerShellFrame;
|
||||
|
||||
@@ -29,12 +32,14 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ViewModel.PropertyChanged += ViewModelPropertyChanged;
|
||||
ManageCalendarDisplayType(ViewModel.StatePersistenceService.CalendarDisplayType);
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateNavigationPaneLayout(navigationView.DisplayMode);
|
||||
SynchronizeVisibleDateRangeCalendar();
|
||||
}
|
||||
|
||||
private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType)
|
||||
@@ -49,9 +54,11 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage());
|
||||
private void PreviousDateClicked(object sender, RoutedEventArgs e)
|
||||
=> ViewModel.PreviousDateRangeCommand.Execute(null);
|
||||
|
||||
private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage());
|
||||
private void NextDateClicked(object sender, RoutedEventArgs e)
|
||||
=> ViewModel.NextDateRangeCommand.Execute(null);
|
||||
|
||||
private async void NewCalendarEventNavigationItemTapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
@@ -59,6 +66,33 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
await InvokeNewCalendarEventAsync();
|
||||
}
|
||||
|
||||
private void VisibleDateRangeCalendarViewSelectedDatesChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
|
||||
{
|
||||
if (_isSynchronizingVisibleDateRangeCalendar)
|
||||
return;
|
||||
|
||||
DateTimeOffset? interactedDate = null;
|
||||
|
||||
if (args.AddedDates.Count > 0)
|
||||
{
|
||||
interactedDate = args.AddedDates[0];
|
||||
}
|
||||
else if (args.RemovedDates.Count > 0)
|
||||
{
|
||||
interactedDate = args.RemovedDates[0];
|
||||
}
|
||||
|
||||
if (interactedDate is null)
|
||||
return;
|
||||
|
||||
var clickedArgs = new CalendarViewDayClickedEventArgs(interactedDate.Value.DateTime);
|
||||
|
||||
if (ViewModel.DateClickedCommand.CanExecute(clickedArgs))
|
||||
{
|
||||
ViewModel.DateClickedCommand.Execute(clickedArgs);
|
||||
}
|
||||
}
|
||||
|
||||
private async void NewCalendarEventNavigationItemKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key is not (VirtualKey.Enter or VirtualKey.Space))
|
||||
@@ -102,6 +136,15 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
public void Receive(CalendarDisplayTypeChangedMessage message)
|
||||
{
|
||||
ManageCalendarDisplayType(message.NewDisplayType);
|
||||
SynchronizeVisibleDateRangeCalendar();
|
||||
}
|
||||
|
||||
private void ViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(ViewModel.CurrentVisibleRange) or nameof(ViewModel.VisibleDateRangeText))
|
||||
{
|
||||
SynchronizeVisibleDateRangeCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
@@ -226,4 +269,39 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract,
|
||||
_ => key.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private void SynchronizeVisibleDateRangeCalendar()
|
||||
{
|
||||
if (!DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
var enqueued = DispatcherQueue.TryEnqueue(SynchronizeVisibleDateRangeCalendar);
|
||||
|
||||
if (!enqueued)
|
||||
throw new InvalidOperationException("Could not marshal visible date range calendar synchronization onto the UI thread.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_isSynchronizingVisibleDateRangeCalendar = true;
|
||||
|
||||
try
|
||||
{
|
||||
VisibleDateRangeCalendarView.SelectedDates.Clear();
|
||||
|
||||
var currentRange = ViewModel.CurrentVisibleRange;
|
||||
if (currentRange == null)
|
||||
return;
|
||||
|
||||
foreach (var date in currentRange.Dates)
|
||||
{
|
||||
VisibleDateRangeCalendarView.SelectedDates.Add(new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue)));
|
||||
}
|
||||
|
||||
VisibleDateRangeCalendarView.SetDisplayDate(new DateTimeOffset(currentRange.AnchorDate.ToDateTime(TimeOnly.MinValue)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSynchronizingVisibleDateRangeCalendar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,210 +1,42 @@
|
||||
using System;
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Wino.Calendar.Args;
|
||||
using Wino.Calendar.Views.Abstract;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Views;
|
||||
|
||||
public sealed partial class CalendarPage : CalendarPageAbstract,
|
||||
IRecipient<ScrollToDateMessage>,
|
||||
IRecipient<ScrollToHourMessage>,
|
||||
IRecipient<GoNextDateRequestedMessage>,
|
||||
IRecipient<GoPreviousDateRequestedMessage>
|
||||
public sealed partial class CalendarPage : CalendarPageAbstract
|
||||
{
|
||||
private const int PopupDialogOffset = 12;
|
||||
|
||||
public CalendarPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged;
|
||||
}
|
||||
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
|
||||
ViewModel.DetailsShowCalendarItemChanged -= CalendarItemDetailContextChanged;
|
||||
QuickEventPopupDialog.IsOpen = false;
|
||||
EventDetailsPopup.IsOpen = false;
|
||||
EventDetailsPopup.PlacementTarget = null;
|
||||
CalendarControl.ResetTimelineSelection();
|
||||
|
||||
if (CalendarControl is IDisposable disposableCalendarControl)
|
||||
{
|
||||
disposableCalendarControl.Dispose();
|
||||
}
|
||||
|
||||
Bindings.StopTracking();
|
||||
}
|
||||
|
||||
private void CalendarItemDetailContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
|
||||
{
|
||||
var control = CalendarControl.GetCalendarItemControl(ViewModel.DisplayDetailsCalendarItemViewModel);
|
||||
|
||||
if (control != null)
|
||||
{
|
||||
EventDetailsPopup.PlacementTarget = control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ScrollToDateMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ScrollToHourMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<GoNextDateRequestedMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<GoPreviousDateRequestedMessage>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
|
||||
WeakReferenceMessenger.Default.Unregister<ScrollToDateMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<ScrollToHourMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<GoNextDateRequestedMessage>(this);
|
||||
WeakReferenceMessenger.Default.Unregister<GoPreviousDateRequestedMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(ScrollToHourMessage message) => DispatcherQueue.TryEnqueue(() => CalendarControl.NavigateToHour(message.TimeSpan));
|
||||
public void Receive(ScrollToDateMessage message) => DispatcherQueue.TryEnqueue(() => CalendarControl.NavigateToDay(message.Date));
|
||||
public void Receive(GoNextDateRequestedMessage message) => DispatcherQueue.TryEnqueue(() => CalendarControl.GoNextRange());
|
||||
public void Receive(GoPreviousDateRequestedMessage message) => DispatcherQueue.TryEnqueue(() => CalendarControl.GoPreviousRange());
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
if (e.NavigationMode == NavigationMode.Back)
|
||||
if (e.NavigationMode == NavigationMode.Back && ViewModel.RestoreVisibleState())
|
||||
{
|
||||
if (ViewModel.RestoreVisibleState())
|
||||
{
|
||||
var restoreDate = ViewModel.GetRestoreDate();
|
||||
DispatcherQueue.TryEnqueue(() => CalendarControl.NavigateToDay(restoreDate));
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(DateTime.Now.Date, CalendarInitInitiative.App));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Parameter is CalendarPageNavigationArgs args)
|
||||
var anchorDate = DateOnly.FromDateTime(DateTime.Now.Date);
|
||||
|
||||
if (e.Parameter is CalendarPageNavigationArgs args && !args.RequestDefaultNavigation)
|
||||
{
|
||||
if (args.RequestDefaultNavigation)
|
||||
{
|
||||
// Go today.
|
||||
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(DateTime.Now.Date, CalendarInitInitiative.App));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Go specified date.
|
||||
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(args.NavigationDate, CalendarInitInitiative.User));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CellSelected(object sender, TimelineCellSelectedArgs e)
|
||||
{
|
||||
// Dismiss event details if exists and cancel the selection.
|
||||
// This is to prevent the event details from being displayed when the user clicks somewhere else.
|
||||
|
||||
if (EventDetailsPopup.IsOpen)
|
||||
{
|
||||
CalendarControl.UnselectActiveTimelineCell();
|
||||
ViewModel.DisplayDetailsCalendarItemViewModel = null;
|
||||
|
||||
return;
|
||||
anchorDate = DateOnly.FromDateTime(args.NavigationDate.Date);
|
||||
}
|
||||
|
||||
ViewModel.SelectedQuickEventDate = e.ClickedDate;
|
||||
|
||||
TeachingTipPositionerGrid.Width = e.CellSize.Width;
|
||||
TeachingTipPositionerGrid.Height = e.CellSize.Height;
|
||||
|
||||
Canvas.SetLeft(TeachingTipPositionerGrid, e.PositionerPoint.X);
|
||||
Canvas.SetTop(TeachingTipPositionerGrid, e.PositionerPoint.Y);
|
||||
|
||||
// Adjust the start and end time in the flyout.
|
||||
var startTime = ViewModel.SelectedQuickEventDate.Value.TimeOfDay;
|
||||
var endTime = startTime.Add(TimeSpan.FromMinutes(30));
|
||||
|
||||
ViewModel.SelectQuickEventTimeRange(startTime, endTime);
|
||||
|
||||
QuickEventPopupDialog.IsOpen = true;
|
||||
}
|
||||
|
||||
private void CellUnselected(object sender, TimelineCellUnselectedArgs e)
|
||||
{
|
||||
QuickEventPopupDialog.IsOpen = false;
|
||||
}
|
||||
|
||||
private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
QuickEventAccountSelectorFlyout.Hide();
|
||||
}
|
||||
|
||||
private void QuickEventPopupClosed(object sender, object e)
|
||||
{
|
||||
// Reset the timeline selection when the tip is closed.
|
||||
CalendarControl.ResetTimelineSelection();
|
||||
}
|
||||
|
||||
private void PopupPlacementChanged(object sender, object e)
|
||||
{
|
||||
if (sender is Popup senderPopup)
|
||||
{
|
||||
// When the quick event Popup is positioned for different calendar types,
|
||||
// we must adjust the offset to make sure the tip is not hidden and has nice
|
||||
// spacing from the cell.
|
||||
|
||||
switch (senderPopup.ActualPlacement)
|
||||
{
|
||||
case PopupPlacementMode.Top:
|
||||
senderPopup.VerticalOffset = PopupDialogOffset * -1;
|
||||
break;
|
||||
case PopupPlacementMode.Bottom:
|
||||
senderPopup.VerticalOffset = PopupDialogOffset;
|
||||
break;
|
||||
case PopupPlacementMode.Left:
|
||||
senderPopup.HorizontalOffset = PopupDialogOffset * -1;
|
||||
break;
|
||||
case PopupPlacementMode.Right:
|
||||
senderPopup.HorizontalOffset = PopupDialogOffset;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
|
||||
=> ViewModel.SelectedStartTimeString = args.Text;
|
||||
|
||||
private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
|
||||
=> ViewModel.SelectedEndTimeString = args.Text;
|
||||
|
||||
private void EventDetailsPopupClosed(object sender, object e)
|
||||
{
|
||||
ViewModel.DisplayDetailsCalendarItemViewModel = null;
|
||||
}
|
||||
|
||||
private void CalendarScrolling(object sender, EventArgs e)
|
||||
{
|
||||
// In case of scrolling, we must dismiss the event details dialog.
|
||||
ViewModel.DisplayDetailsCalendarItemViewModel = null;
|
||||
var request = new CalendarDisplayRequest(ViewModel.StatePersistanceService.CalendarDisplayType, anchorDate);
|
||||
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(request));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Settings;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
@@ -51,11 +51,6 @@ public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract
|
||||
ViewModel.NavigateToAddAccount();
|
||||
}
|
||||
|
||||
private void WinoAccountManagementClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.NavigateToWinoAccountManagementCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||
{
|
||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput || string.IsNullOrWhiteSpace(sender.Text))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Views.Abstract"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -93,8 +92,13 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<controls:MarkdownTextBlock Text="{x:Bind Title, Mode=OneTime}" />
|
||||
<controls:MarkdownTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Description, Mode=OneTime}" />
|
||||
<TextBlock
|
||||
Text="{x:Bind Title, Mode=OneTime}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Description, Mode=OneTime}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -568,11 +568,26 @@
|
||||
Text="{x:Bind domain:Translator.CalendarEventCompose_NewEventButton, Mode=OneTime}" />
|
||||
</coreControls:WinoNavigationViewItem>
|
||||
|
||||
<calendarControls:WinoCalendarView
|
||||
x:Name="CalendarView"
|
||||
<Border
|
||||
x:Name="RangeSummaryCard"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
TodayBackgroundColor="{ThemeResource SystemAccentColor}" />
|
||||
Margin="16,0,16,12"
|
||||
Padding="12"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.CalendarClient.VisibleDateRangeText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
|
||||
<muxc:CalendarView
|
||||
x:Name="VisibleDateRangeCalendarView"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionMode="Multiple"
|
||||
SelectedDatesChanged="VisibleDateRangeCalendarViewSelectedDatesChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ListView
|
||||
x:Name="CalendarHostListView"
|
||||
@@ -764,7 +779,7 @@
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ShellContentArea.Visibility" Value="Collapsed" />
|
||||
<Setter Target="CalendarTypeSelector.Visibility" Value="Collapsed" />
|
||||
<Setter Target="CalendarView.IsEnabled" Value="False" />
|
||||
<Setter Target="RangeSummaryCard.IsEnabled" Value="False" />
|
||||
<Setter Target="CalendarHostListView.IsEnabled" Value="False" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
@@ -12,19 +12,18 @@ using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
using Wino.Calendar.Controls;
|
||||
using Wino.Calendar.Views;
|
||||
using Wino.Calendar.ViewModels;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.MenuItems;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Mail.ViewModels;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.WinUI.Controls;
|
||||
using Wino.MenuFlyouts;
|
||||
@@ -34,10 +33,7 @@ using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
using Wino.Messaging.UI;
|
||||
using Wino.Views.Mail;
|
||||
using Wino.Views;
|
||||
using Wino.Views.Settings;
|
||||
using Windows.System;
|
||||
|
||||
namespace Wino.Mail.WinUI.Views;
|
||||
|
||||
@@ -55,6 +51,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
private const string StateEventDetailsContent = "EventDetailsContentState";
|
||||
private WinoApplicationMode? _activeMode;
|
||||
private bool _isSyncingNavigationViewSelection;
|
||||
private bool _isSynchronizingVisibleDateRangeCalendar;
|
||||
|
||||
public WinoAppShell()
|
||||
{
|
||||
@@ -203,7 +200,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
|
||||
if (ViewModel.IsCalendarMode)
|
||||
{
|
||||
ViewModel.StatePersistenceService.CoreWindowTitle = string.Empty;
|
||||
ViewModel.StatePersistenceService.CoreWindowTitle = ViewModel.CalendarClient.VisibleDateRangeText ?? string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +224,6 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
private void InitializeCalendarControls()
|
||||
{
|
||||
CalendarTypeSelector.TodayClickedCommand = ViewModel.CalendarClient.TodayClickedCommand;
|
||||
CalendarView.DateClickedCommand = ViewModel.CalendarClient.DateClickedCommand;
|
||||
DayHeaderNavigationItemsFlipView.ItemsSource = ViewModel.CalendarClient.DateNavigationHeaderItems;
|
||||
CalendarHostListView.ItemsSource = ViewModel.CalendarClient.GroupedAccountCalendars;
|
||||
|
||||
@@ -239,8 +235,8 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
DayHeaderNavigationItemsFlipView.ItemsSource = ViewModel.CalendarClient.DateNavigationHeaderItems;
|
||||
DayHeaderNavigationItemsFlipView.SelectedIndex = ViewModel.CalendarClient.SelectedDateNavigationHeaderIndex;
|
||||
CalendarTypeSelector.DisplayDayCount = ViewModel.CalendarClient.StatePersistenceService.DayDisplayCount;
|
||||
CalendarView.HighlightedDateRange = ViewModel.CalendarClient.HighlightedDateRange;
|
||||
CalendarHostListView.ItemsSource = ViewModel.CalendarClient.GroupedAccountCalendars;
|
||||
SynchronizeVisibleDateRangeCalendar();
|
||||
}
|
||||
|
||||
private void RefreshNavigationViewBindings(bool syncMailSelection = true)
|
||||
@@ -283,9 +279,11 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
await InvokeNewCalendarEventAsync();
|
||||
}
|
||||
|
||||
private void PreviousDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoPreviousDateRequestedMessage());
|
||||
private void PreviousDateClicked(object sender, RoutedEventArgs e)
|
||||
=> ViewModel.CalendarClient.PreviousDateRangeCommand.Execute(null);
|
||||
|
||||
private void NextDateClicked(object sender, RoutedEventArgs e) => WeakReferenceMessenger.Default.Send(new GoNextDateRequestedMessage());
|
||||
private void NextDateClicked(object sender, RoutedEventArgs e)
|
||||
=> ViewModel.CalendarClient.NextDateRangeCommand.Execute(null);
|
||||
|
||||
private Task InvokeNewCalendarEventAsync()
|
||||
=> ViewModel.CalendarClient.HandleNavigationItemInvokedAsync(new NewCalendarEventMenuItem());
|
||||
@@ -512,13 +510,76 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PropertyName == nameof(ICalendarShellClient.HighlightedDateRange))
|
||||
if (e.PropertyName == nameof(ICalendarShellClient.CurrentVisibleRange) ||
|
||||
e.PropertyName == nameof(ICalendarShellClient.VisibleDateRangeText))
|
||||
{
|
||||
CalendarView.HighlightedDateRange = ViewModel.CalendarClient.HighlightedDateRange;
|
||||
SynchronizeVisibleDateRangeCalendar();
|
||||
UpdateTitleBarSubtitle();
|
||||
}
|
||||
}
|
||||
|
||||
private void VisibleDateRangeCalendarViewSelectedDatesChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
|
||||
{
|
||||
if (_isSynchronizingVisibleDateRangeCalendar)
|
||||
return;
|
||||
|
||||
DateTimeOffset? interactedDate = null;
|
||||
|
||||
if (args.AddedDates.Count > 0)
|
||||
{
|
||||
interactedDate = args.AddedDates[0];
|
||||
}
|
||||
else if (args.RemovedDates.Count > 0)
|
||||
{
|
||||
interactedDate = args.RemovedDates[0];
|
||||
}
|
||||
|
||||
if (interactedDate is null)
|
||||
return;
|
||||
|
||||
var clickedArgs = new CalendarViewDayClickedEventArgs(interactedDate.Value.DateTime);
|
||||
|
||||
if (ViewModel.CalendarClient.DateClickedCommand.CanExecute(clickedArgs))
|
||||
{
|
||||
ViewModel.CalendarClient.DateClickedCommand.Execute(clickedArgs);
|
||||
}
|
||||
}
|
||||
|
||||
private void SynchronizeVisibleDateRangeCalendar()
|
||||
{
|
||||
if (!DispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
var enqueued = DispatcherQueue.TryEnqueue(SynchronizeVisibleDateRangeCalendar);
|
||||
|
||||
if (!enqueued)
|
||||
throw new InvalidOperationException("Could not marshal visible date range calendar synchronization onto the UI thread.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_isSynchronizingVisibleDateRangeCalendar = true;
|
||||
|
||||
try
|
||||
{
|
||||
VisibleDateRangeCalendarView.SelectedDates.Clear();
|
||||
|
||||
var currentRange = ViewModel.CalendarClient.CurrentVisibleRange;
|
||||
if (currentRange == null)
|
||||
return;
|
||||
|
||||
foreach (var date in currentRange.Dates)
|
||||
{
|
||||
VisibleDateRangeCalendarView.SelectedDates.Add(new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue)));
|
||||
}
|
||||
|
||||
VisibleDateRangeCalendarView.SetDisplayDate(new DateTimeOffset(currentRange.AnchorDate.ToDateTime(TimeOnly.MinValue)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSynchronizingVisibleDateRangeCalendar = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(ViewModel.SelectedMenuItem) || !ViewModel.CurrentClient.HandlesNavigationSelection)
|
||||
|
||||
@@ -119,10 +119,7 @@
|
||||
<None Remove="Styles\CalendarShellNavigationViewStyle.xaml" />
|
||||
<None Remove="Styles\CalendarThemeResources.xaml" />
|
||||
<None Remove="Styles\DayHeaderControl.xaml" />
|
||||
<None Remove="Styles\WinoCalendarResources.xaml" />
|
||||
<None Remove="Styles\WinoCalendarTypeSelectorControl.xaml" />
|
||||
<None Remove="Styles\WinoCalendarView.xaml" />
|
||||
<None Remove="Styles\WinoDayTimelineCanvas.xaml" />
|
||||
<None Remove="Views\Calendar\CalendarAppShell.xaml" />
|
||||
<None Remove="Views\Calendar\CalendarPage.xaml" />
|
||||
<None Remove="Views\Calendar\CalendarSettingsPage.xaml" />
|
||||
@@ -205,10 +202,8 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
|
||||
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
||||
<PackageReference Include="SkiaSharp.Views.WinUI" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
@@ -362,18 +357,9 @@
|
||||
<Page Update="Styles\DayHeaderControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\WinoCalendarResources.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\WinoCalendarTypeSelectorControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\WinoCalendarView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\WinoDayTimelineCanvas.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Views\Calendar\CalendarAppShell.xaml">
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Wino.Messaging.Client.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when user clicks '>' button from the shell.
|
||||
/// </summary>
|
||||
public record GoNextDateRequestedMessage;
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Wino.Messaging.Client.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when user clicks '<' button from the shell.
|
||||
/// </summary>
|
||||
public record GoPreviousDateRequestedMessage;
|
||||
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Messaging.Client.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a new calendar range is requested for drawing.
|
||||
/// Raised when a new calendar display range is requested.
|
||||
/// </summary>
|
||||
/// <param name="DisplayType">Type of the calendar.</param>
|
||||
/// <param name="DisplayDate">Exact date to highlight.</param>
|
||||
/// <param name="DayDisplayCount">How many days to load with Day calendar display type.</param>
|
||||
/// <param name="ForceRedraw">Remove all days and force re-render of everything. Used when settings are updated.</param>
|
||||
public record LoadCalendarMessage(DateTime DisplayDate, CalendarInitInitiative CalendarInitInitiative, bool ForceRedraw = false);
|
||||
/// <param name="DisplayRequest">Display type and anchor date to resolve.</param>
|
||||
/// <param name="ForceReload">Force a reload even if the resolved range did not change.</param>
|
||||
public record LoadCalendarMessage(CalendarDisplayRequest DisplayRequest, bool ForceReload = false);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Messaging.Client.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when requested date is already loaded into calendar flip view to scroll to it.
|
||||
/// </summary>
|
||||
/// <param name="Date">Date to scroll.</param>
|
||||
public record ScrollToDateMessage(DateTime Date);
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Messaging.Client.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Emitted when vertical scroll position is requested to be changed.
|
||||
/// </summary>
|
||||
/// <param name="TimeSpan">Hour to scroll vertically on flip view item.</param>
|
||||
public record ScrollToHourMessage(TimeSpan TimeSpan);
|
||||
@@ -1,10 +0,0 @@
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Messaging.Client.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when calendar's visible date range is updated.
|
||||
/// Used to update the background of the visible date range in CalendarView.
|
||||
/// </summary>
|
||||
/// <param name="DateRange">New visible date range.</param>
|
||||
public record VisibleDateRangeChangedMessage(DateRange DateRange);
|
||||
Reference in New Issue
Block a user