diff --git a/Wino.Calendar.ViewModels/AppShellViewModel.cs b/Wino.Calendar.ViewModels/AppShellViewModel.cs index 376ec624..a938c35c 100644 --- a/Wino.Calendar.ViewModels/AppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/AppShellViewModel.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Wino.Calendar.ViewModels.Data; +using Wino.Calendar.ViewModels.Interfaces; using Wino.Core.Domain.Collections; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; @@ -26,6 +29,7 @@ namespace Wino.Calendar.ViewModels public event EventHandler DisplayTypeChanged; public IPreferencesService PreferencesService { get; } public IStatePersistanceService StatePersistenceService { get; } + public IAccountCalendarStateService AccountCalendarStateService { get; } public INavigationService NavigationService { get; } public IWinoServerConnectionManager ServerConnectionManager { get; } @@ -63,9 +67,16 @@ namespace Wino.Calendar.ViewModels public AppShellViewModel(IPreferencesService preferencesService, IStatePersistanceService statePersistanceService, + IAccountService accountService, + ICalendarService calendarService, + IAccountCalendarStateService accountCalendarStateService, INavigationService navigationService, IWinoServerConnectionManager serverConnectionManager) { + _accountService = accountService; + _calendarService = calendarService; + + AccountCalendarStateService = accountCalendarStateService; NavigationService = navigationService; ServerConnectionManager = serverConnectionManager; PreferencesService = preferencesService; @@ -86,11 +97,53 @@ namespace Wino.Calendar.ViewModels } } - public override void OnNavigatedTo(NavigationMode mode, object parameters) + public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); UpdateDateNavigationHeaderItems(); + + await InitializeAccountCalendarsAsync(); + } + + private void AddGroupedAccountCalendarViewModel(GroupedAccountCalendarViewModel groupedAccountCalendarViewModel) + { + foreach (var calendarViewModel in groupedAccountCalendarViewModel.AccountCalendars) + { + calendarViewModel.CalendarSelectionStateChanged += UpdateAccountCalendarRequested; + } + + AccountCalendarStateService.GroupedAccountCalendars.Add(groupedAccountCalendarViewModel); + } + + private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) + => await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false); + + private async Task InitializeAccountCalendarsAsync() + { + await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.GroupedAccountCalendars.Clear()); + + var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false); + + foreach (var account in accounts) + { + var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false); + var calendarViewModels = new List(); + + foreach (var calendar in accountCalendars) + { + var calendarViewModel = new AccountCalendarViewModel(account, calendar); + + calendarViewModels.Add(calendarViewModel); + } + + var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels); + + await Dispatcher.ExecuteOnUIThread(() => + { + AddGroupedAccountCalendarViewModel(groupedAccountCalendarViewModel); + }); + } } private void ForceNavigateCalendarDate() @@ -177,6 +230,8 @@ namespace Wino.Calendar.ViewModels } private DateTime? _navigationDate; + private readonly IAccountService _accountService; + private readonly ICalendarService _calendarService; public override void OnPageLoaded() { diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 5b1474ca..77cc9e3b 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -7,18 +7,21 @@ using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; +using Wino.Calendar.ViewModels.Interfaces; using Wino.Core.Domain.Collections; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies; +using Wino.Core.Domain.Models.Navigation; using Wino.Core.ViewModels; using Wino.Messaging.Client.Calendar; namespace Wino.Calendar.ViewModels { public partial class CalendarPageViewModel : CalendarBaseViewModel, - IRecipient + IRecipient, + IRecipient { [ObservableProperty] private ObservableRangeCollection _dayRanges = []; @@ -32,9 +35,11 @@ namespace Wino.Calendar.ViewModels [ObservableProperty] private bool _isCalendarEnabled = true; - // Get rid of some of the items if we have too many. + // TODO: Get rid of some of the items if we have too many. private const int maxDayRangeSize = 10; + private readonly ICalendarService _calendarService; + private readonly IAccountCalendarStateService _accountCalendarStateService; private readonly IPreferencesService _preferencesService; // Store latest rendered options. @@ -49,13 +54,14 @@ namespace Wino.Calendar.ViewModels public CalendarPageViewModel(IStatePersistanceService statePersistanceService, ICalendarService calendarService, + IAccountCalendarStateService accountCalendarStateService, IPreferencesService preferencesService) { StatePersistanceService = statePersistanceService; - _calendarService = calendarService; - _preferencesService = preferencesService; - _currentSettings = _preferencesService.GetCurrentCalendarSettings(); + _calendarService = calendarService; + _accountCalendarStateService = accountCalendarStateService; + _preferencesService = preferencesService; } // TODO: Replace when calendar settings are updated. @@ -70,10 +76,25 @@ namespace Wino.Calendar.ViewModels }; } + public override void OnNavigatedFrom(NavigationMode mode, object parameters) + { + // Do not call base method because that will unregister messenger recipient. + // This is a singleton view model and should not be unregistered. + } + + public override void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + _currentSettings = _preferencesService.GetCurrentCalendarSettings(); + } + partial void OnIsCalendarEnabledChanging(bool oldValue, bool newValue) => Messenger.Send(new CalendarEnableStatusChangedMessage(newValue)); private bool ShouldResetDayRanges(LoadCalendarMessage message) { + if (message.ForceRedraw) return true; + // Never reset if the initiative is from the app. if (message.CalendarInitInitiative == CalendarInitInitiative.App) return false; @@ -168,14 +189,14 @@ namespace Wino.Calendar.ViewModels DateRange flipLoadRange = null; - if (calendarInitInitiative == CalendarInitInitiative.User) + var initializedDateRange = GetLoadedDateRange(); + + if (calendarInitInitiative == CalendarInitInitiative.User || initializedDateRange == null) { flipLoadRange = strategy.GetRenderDateRange(displayDate, StatePersistanceService.DayDisplayCount); } else { - var initializedDateRange = GetLoadedDateRange(); - // App is trying to load. // This should be based on direction. We'll load the next or previous range. // DisplayDate is either the start or end date of the current visible range. @@ -214,7 +235,7 @@ namespace Wino.Calendar.ViewModels { foreach (var day in renderModel.CalendarDays) { - var events = await _calendarService.GetCalendarEventsAsync(Guid.Parse("13e8e385-a1bb-4764-95b4-757901cad35a"), day.Period.Start, day.Period.End).ConfigureAwait(false); + var events = await _calendarService.GetCalendarEventsAsync(Guid.Parse("9ead7613-dacb-4163-8d33-2e32e65008a1"), day.Period.Start, day.Period.End).ConfigureAwait(false); foreach (var calendarItem in events) { @@ -343,6 +364,8 @@ namespace Wino.Calendar.ViewModels var initializedDateRange = GetLoadedDateRange(); + if (initializedDateRange == null) return false; + var selectedDate = message.DisplayDate; return selectedDate >= initializedDateRange.StartDate && selectedDate <= initializedDateRange.EndDate; @@ -391,67 +414,14 @@ namespace Wino.Calendar.ViewModels } } - protected override async void OnCalendarEventAdded(ICalendarItem calendarItem) + public void Receive(CalendarSettingsUpdatedMessage message) { - base.OnCalendarEventAdded(calendarItem); + _currentSettings = _preferencesService.GetCurrentCalendarSettings(); - // Test - //var eventDays = DayRanges.SelectMany(a => a.CalendarDays).Where(b => b.Period.Start.Date == calendarItem.StartTime.Date); + // TODO: This might need throttling due to slider in the settings page for hour height. + // or make sure the slider does not update on each tick but on focus lost. - //var beforeAllDay = new CalendarItem(calendarItem.StartTime.Date.AddHours(0), calendarItem.StartTime.Date.AddMinutes(30)) - //{ - // Title = "kj" - //}; - - //var allday = new CalendarItem(calendarItem.StartTime.Date.AddHours(1), calendarItem.StartTime.Date.AddHours(10).AddMinutes(59)) - //{ - // Title = "All day" - //}; - - //var test = new CalendarItem(calendarItem.StartTime.Date.AddHours(4), calendarItem.StartTime.Date.AddHours(4).AddMinutes(30)) - //{ - // Title = "test" - //}; - - //var hour = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(8)) - //{ - // Title = "1 h" - //}; - - //var hourandhalf = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(8).AddMinutes(30)) - //{ - // Title = "1.5 h" - //}; - //var halfhour1 = new CalendarItem(calendarItem.StartTime.Date.AddHours(7), calendarItem.StartTime.Date.AddHours(7).AddMinutes(30)) - //{ - // Title = "30 min" - //}; - - //var halfhour2 = new CalendarItem(calendarItem.StartTime.Date.AddHours(7).AddMinutes(30), calendarItem.StartTime.Date.AddHours(8)) - //{ - // Title = "30 min" - //}; - //var halfhour3 = new CalendarItem(calendarItem.StartTime.Date.AddHours(8), calendarItem.StartTime.Date.AddHours(8).AddMinutes(30)) - //{ - // Title = "30 min" - //}; - - //foreach (var day in eventDays) - //{ - // await ExecuteUIThread(() => - // { - // day.Events.Add(beforeAllDay); - // day.Events.Add(allday); - // day.Events.Add(hourandhalf); - // day.Events.Add(hour); - // day.Events.Add(halfhour1); - // day.Events.Add(halfhour2); - // day.Events.Add(halfhour3); - // day.Events.Add(test); - // }); - //} - - //return; + Messenger.Send(new LoadCalendarMessage(DateTime.UtcNow.Date, CalendarInitInitiative.App, true)); } } } diff --git a/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs new file mode 100644 index 00000000..26f687d5 --- /dev/null +++ b/Wino.Calendar.ViewModels/Data/AccountCalendarViewModel.cs @@ -0,0 +1,75 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Entities.Calendar; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Calendar.ViewModels.Data +{ + public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar + { + public event EventHandler CalendarSelectionStateChanged; + + public MailAccount Account { get; } + public AccountCalendar AccountCalendar { get; } + + public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar) + { + Account = account; + AccountCalendar = accountCalendar; + + IsChecked = accountCalendar.IsExtended; + } + + [ObservableProperty] + private bool _isChecked; + + partial void OnIsCheckedChanged(bool value) + { + IsExtended = value; + CalendarSelectionStateChanged?.Invoke(this, this); + } + + public string Name + { + get => AccountCalendar.Name; + set => SetProperty(AccountCalendar.Name, value, AccountCalendar, (u, n) => u.Name = n); + } + + public string TextColorHex + { + get => AccountCalendar.TextColorHex; + set => SetProperty(AccountCalendar.TextColorHex, value, AccountCalendar, (u, t) => u.TextColorHex = t); + } + + public string BackgroundColorHex + { + get => AccountCalendar.BackgroundColorHex; + set => SetProperty(AccountCalendar.BackgroundColorHex, value, AccountCalendar, (u, b) => u.BackgroundColorHex = b); + } + + public bool IsExtended + { + get => AccountCalendar.IsExtended; + set => SetProperty(AccountCalendar.IsExtended, value, AccountCalendar, (u, i) => u.IsExtended = i); + } + + public bool IsPrimary + { + get => AccountCalendar.IsPrimary; + set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i); + } + + public Guid AccountId + { + get => AccountCalendar.AccountId; + set => SetProperty(AccountCalendar.AccountId, value, AccountCalendar, (u, a) => u.AccountId = a); + } + + public string RemoteCalendarId + { + get => AccountCalendar.RemoteCalendarId; + set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r); + } + } +} diff --git a/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs new file mode 100644 index 00000000..6d13dc78 --- /dev/null +++ b/Wino.Calendar.ViewModels/Data/GroupedAccountCalendarViewModel.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain.Entities.Shared; + +namespace Wino.Calendar.ViewModels.Data +{ + public partial class GroupedAccountCalendarViewModel : ObservableObject + { + public MailAccount Account { get; } + public ObservableCollection AccountCalendars { get; } + + public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable calendarViewModels) + { + Account = account; + AccountCalendars = new ObservableCollection(calendarViewModels); + + ManageIsCheckedState(); + + foreach (var calendarViewModel in calendarViewModels) + { + calendarViewModel.PropertyChanged += CalendarPropertyChanged; + } + + AccountCalendars.CollectionChanged += CalendarListUpdated; + } + + private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (AccountCalendarViewModel calendar in e.NewItems) + { + calendar.PropertyChanged += CalendarPropertyChanged; + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (AccountCalendarViewModel calendar in e.OldItems) + { + calendar.PropertyChanged -= CalendarPropertyChanged; + } + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + foreach (AccountCalendarViewModel calendar in e.OldItems) + { + calendar.PropertyChanged -= CalendarPropertyChanged; + } + } + } + + private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (sender is AccountCalendarViewModel viewModel) + { + if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked)) + { + ManageIsCheckedState(); + } + } + } + + [ObservableProperty] + private bool _isExpanded = true; + + [ObservableProperty] + private bool? isCheckedState = true; + + private bool _isExternalPropChangeBlocked = false; + + private void ManageIsCheckedState() + { + _isExternalPropChangeBlocked = true; + + if (AccountCalendars.All(c => c.IsChecked)) + { + IsCheckedState = true; + } + else if (AccountCalendars.All(c => !c.IsChecked)) + { + IsCheckedState = false; + } + else + { + IsCheckedState = null; + } + + _isExternalPropChangeBlocked = false; + } + + partial void OnIsCheckedStateChanged(bool? newValue) + { + if (_isExternalPropChangeBlocked) return; + + if (newValue == null) + { + // Only primary calendars must be checked. + + foreach (var calendar in AccountCalendars) + { + calendar.IsChecked = calendar.IsPrimary; + } + } + else + { + foreach (var calendar in AccountCalendars) + { + calendar.IsChecked = newValue.GetValueOrDefault(); + } + } + } + } +} diff --git a/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs new file mode 100644 index 00000000..d97d614b --- /dev/null +++ b/Wino.Calendar.ViewModels/Interfaces/IAccountCalendarStateService.cs @@ -0,0 +1,10 @@ +using System.Collections.ObjectModel; +using Wino.Calendar.ViewModels.Data; + +namespace Wino.Calendar.ViewModels.Interfaces +{ + public interface IAccountCalendarStateService + { + ObservableCollection GroupedAccountCalendars { get; } + } +} diff --git a/Wino.Calendar/App.xaml.cs b/Wino.Calendar/App.xaml.cs index 86fa9960..e7ca3f84 100644 --- a/Wino.Calendar/App.xaml.cs +++ b/Wino.Calendar/App.xaml.cs @@ -12,6 +12,7 @@ using Wino.Activation; using Wino.Calendar.Activation; using Wino.Calendar.Services; using Wino.Calendar.ViewModels; +using Wino.Calendar.ViewModels.Interfaces; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; @@ -71,6 +72,7 @@ namespace Wino.Calendar services.AddTransient(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); } private void RegisterViewModels(IServiceCollection services) diff --git a/Wino.Calendar/Services/AccountCalendarStateService.cs b/Wino.Calendar/Services/AccountCalendarStateService.cs new file mode 100644 index 00000000..2520d947 --- /dev/null +++ b/Wino.Calendar/Services/AccountCalendarStateService.cs @@ -0,0 +1,13 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Calendar.ViewModels.Data; +using Wino.Calendar.ViewModels.Interfaces; + +namespace Wino.Calendar.Services +{ + public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService + { + [ObservableProperty] + private ObservableCollection _groupedAccountCalendars = new ObservableCollection(); + } +} diff --git a/Wino.Calendar/Views/AppShell.xaml b/Wino.Calendar/Views/AppShell.xaml index e7d875f7..127ce31d 100644 --- a/Wino.Calendar/Views/AppShell.xaml +++ b/Wino.Calendar/Views/AppShell.xaml @@ -9,6 +9,7 @@ xmlns:coreControls="using:Wino.Core.UWP.Controls" xmlns:coreSelectors="using:Wino.Core.UWP.Selectors" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:data="using:Wino.Calendar.ViewModels.Data" xmlns:domain="using:Wino.Core.Domain" xmlns:helpers="using:Wino.Helpers" xmlns:local="using:Wino.Calendar.Views" @@ -174,20 +175,87 @@ HighlightedDateRange="{x:Bind ViewModel.HighlightedDateRange, Mode=OneWay}" TodayBackgroundColor="{ThemeResource SystemAccentColor}" /> - + - + ItemsSource="{x:Bind ViewModel.AccountCalendarStateService.GroupedAccountCalendars}" + SelectionMode="None"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +