Merge branch 'feature/EventCompose' into feature/vNext
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
|
||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
|
||||
<PackageVersion Include="MimeKit" Version="4.14.0" />
|
||||
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
||||
<PackageVersion Include="morelinq" Version="4.4.0" />
|
||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
@@ -74,4 +74,4 @@
|
||||
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -35,9 +35,6 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
||||
[ObservableProperty]
|
||||
public partial bool IsSyncEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsPrimaryCalendar { get; set; }
|
||||
|
||||
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -82,7 +79,6 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
||||
// Initialize properties from AccountCalendar
|
||||
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
|
||||
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
|
||||
IsPrimaryCalendar = AccountCalendar.IsPrimary;
|
||||
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
|
||||
}
|
||||
|
||||
@@ -104,15 +100,6 @@ public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewMode
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnIsPrimaryCalendarChanged(bool value)
|
||||
{
|
||||
if (AccountCalendar != null)
|
||||
{
|
||||
AccountCalendar.IsPrimary = value;
|
||||
SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
|
||||
{
|
||||
if (AccountCalendar != null && value != null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
@@ -8,7 +9,9 @@ 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.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
@@ -73,7 +76,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
ICalendarService calendarService,
|
||||
IAccountCalendarStateService accountCalendarStateService,
|
||||
INavigationService navigationService,
|
||||
IDialogServiceBase dialogService,
|
||||
IMailDialogService dialogService,
|
||||
IUpdateManager updateManager)
|
||||
{
|
||||
_accountService = accountService;
|
||||
@@ -120,12 +123,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
if (mode == NavigationMode.Back)
|
||||
{
|
||||
await InitializeAccountCalendarsAsync();
|
||||
ValidateConfiguredNewEventCalendar();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateDateNavigationHeaderItems();
|
||||
|
||||
await InitializeAccountCalendarsAsync();
|
||||
ValidateConfiguredNewEventCalendar();
|
||||
|
||||
await ShowWhatIsNewIfNeededAsync();
|
||||
|
||||
@@ -290,7 +295,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
private DateTime? _navigationDate;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IDialogServiceBase _dialogService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IUpdateManager _updateManager;
|
||||
|
||||
#region Commands
|
||||
@@ -306,6 +311,47 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
[RelayCommand]
|
||||
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
|
||||
|
||||
[RelayCommand]
|
||||
private async Task NewEventAsync()
|
||||
{
|
||||
var pickedCalendar = TryResolveConfiguredNewEventCalendar();
|
||||
|
||||
if (pickedCalendar == null)
|
||||
{
|
||||
var availableGroups = AccountCalendarStateService.GroupedAccountCalendars
|
||||
.Where(group => group.AccountCalendars.Count > 0)
|
||||
.Select(group => new CalendarPickerAccountGroup
|
||||
{
|
||||
Account = group.Account,
|
||||
Calendars = group.AccountCalendars.Select(calendar => calendar.AccountCalendar).ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (availableGroups.Count == 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.CalendarEventCompose_NoCalendarsTitle,
|
||||
Translator.CalendarEventCompose_NoCalendarsMessage,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
pickedCalendar = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups);
|
||||
}
|
||||
|
||||
if (pickedCalendar == null)
|
||||
return;
|
||||
|
||||
var (startDate, endDate) = GetDefaultComposeDateRange();
|
||||
|
||||
NavigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
|
||||
{
|
||||
SelectedCalendarId = pickedCalendar.Id,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
@@ -435,5 +481,57 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
}
|
||||
|
||||
public async void Receive(AccountRemovedMessage message)
|
||||
=> await InitializeAccountCalendarsAsync();
|
||||
{
|
||||
await InitializeAccountCalendarsAsync();
|
||||
ValidateConfiguredNewEventCalendar();
|
||||
}
|
||||
|
||||
private AccountCalendar TryResolveConfiguredNewEventCalendar()
|
||||
{
|
||||
ValidateConfiguredNewEventCalendar();
|
||||
|
||||
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|
||||
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return AccountCalendarStateService.AllCalendars
|
||||
.FirstOrDefault(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value)?
|
||||
.AccountCalendar;
|
||||
}
|
||||
|
||||
private void ValidateConfiguredNewEventCalendar()
|
||||
{
|
||||
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|
||||
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var exists = AccountCalendarStateService.AllCalendars
|
||||
.Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value);
|
||||
|
||||
if (exists)
|
||||
return;
|
||||
|
||||
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
|
||||
PreferencesService.DefaultNewEventCalendarId = null;
|
||||
}
|
||||
|
||||
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
|
||||
{
|
||||
var localNow = DateTime.Now;
|
||||
var roundedMinutes = localNow.Minute switch
|
||||
{
|
||||
< 30 => 30,
|
||||
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
|
||||
_ => 60
|
||||
};
|
||||
|
||||
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
|
||||
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
|
||||
|
||||
return (startDate, startDate.AddMinutes(30));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,709 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using EmailValidation;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Validation;
|
||||
using Wino.Core.ViewModels;
|
||||
|
||||
namespace Wino.Calendar.ViewModels;
|
||||
|
||||
public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly INavigationService _navigationService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
private readonly IContactService _contactService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||
private readonly CalendarEventComposeResultValidator _composeResultValidator = new();
|
||||
|
||||
public Func<Task<string>> GetHtmlNotesAsync { get; set; }
|
||||
|
||||
public ObservableCollection<AccountCalendarViewModel> AvailableCalendars { get; } = [];
|
||||
public ObservableCollection<GroupedAccountCalendarViewModel> AvailableCalendarGroups { get; } = [];
|
||||
public ObservableCollection<CalendarComposeAttendeeViewModel> Attendees { get; } = [];
|
||||
public ObservableCollection<CalendarComposeAttachmentViewModel> Attachments { get; } = [];
|
||||
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = [];
|
||||
public ObservableCollection<ReminderOption> ReminderOptions { get; } = [];
|
||||
public ObservableCollection<int> RecurrenceIntervalOptions { get; } = [];
|
||||
public ObservableCollection<CalendarComposeFrequencyOption> RecurrenceFrequencyOptions { get; } = [];
|
||||
public ObservableCollection<CalendarComposeWeekdayOption> WeekdayOptions { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendarViewModel SelectedCalendar { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Title { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Location { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsAllDay { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DateTimeOffset StartDate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TimeSpan StartTime { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TimeSpan EndTime { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DateTimeOffset AllDayEndDate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsRecurring { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedRecurrenceInterval { get; set; } = 1;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CalendarComposeFrequencyOption SelectedRecurrenceFrequencyOption { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DateTimeOffset? RecurrenceEndDate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string RecurrenceSummary { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ReminderOption SelectedReminderOption { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ShowAsOption SelectedShowAsOption { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsDarkWebviewRenderer { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CalendarEventComposeResult LastCreatedResult { get; set; }
|
||||
|
||||
public CalendarSettings CurrentSettings { get; }
|
||||
public string TimePickerClockIdentifier => CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "24HourClock" : "12HourClock";
|
||||
public bool HasAttachments => Attachments.Count > 0;
|
||||
public string SelectedCalendarDisplayText => SelectedCalendar?.Name ?? Translator.CalendarEventCompose_SelectCalendar;
|
||||
public string SelectedCalendarAccountText => SelectedCalendar?.Account?.Address ?? string.Empty;
|
||||
|
||||
public CalendarEventComposePageViewModel(IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
INavigationService navigationService,
|
||||
IMailDialogService dialogService,
|
||||
IContactService contactService,
|
||||
IPreferencesService preferencesService,
|
||||
IUnderlyingThemeService underlyingThemeService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_calendarService = calendarService;
|
||||
_navigationService = navigationService;
|
||||
_dialogService = dialogService;
|
||||
_contactService = contactService;
|
||||
_preferencesService = preferencesService;
|
||||
_underlyingThemeService = underlyingThemeService;
|
||||
|
||||
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
|
||||
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
|
||||
|
||||
Attachments.CollectionChanged += AttachmentsCollectionChanged;
|
||||
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
|
||||
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
|
||||
|
||||
foreach (var reminderMinutes in _calendarService.GetPredefinedReminderMinutes().OrderByDescending(x => x))
|
||||
{
|
||||
ReminderOptions.Add(new ReminderOption(reminderMinutes));
|
||||
}
|
||||
|
||||
foreach (var interval in Enumerable.Range(1, 99))
|
||||
{
|
||||
RecurrenceIntervalOptions.Add(interval);
|
||||
}
|
||||
|
||||
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Daily, Translator.CalendarEventCompose_FrequencyDay));
|
||||
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Weekly, Translator.CalendarEventCompose_FrequencyWeek));
|
||||
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Monthly, Translator.CalendarEventCompose_FrequencyMonth));
|
||||
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Yearly, Translator.CalendarEventCompose_FrequencyYear));
|
||||
SelectedRecurrenceFrequencyOption = RecurrenceFrequencyOptions.FirstOrDefault();
|
||||
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Monday, "MO", Translator.CalendarEventCompose_Weekday_Monday));
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Tuesday, "TU", Translator.CalendarEventCompose_Weekday_Tuesday));
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Wednesday, "WE", Translator.CalendarEventCompose_Weekday_Wednesday));
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Thursday, "TH", Translator.CalendarEventCompose_Weekday_Thursday));
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Friday, "FR", Translator.CalendarEventCompose_Weekday_Friday));
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Saturday, "SA", Translator.CalendarEventCompose_Weekday_Saturday));
|
||||
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Sunday, "SU", Translator.CalendarEventCompose_Weekday_Sunday));
|
||||
|
||||
SelectedReminderOption = GetDefaultReminderOption();
|
||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy);
|
||||
|
||||
var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
|
||||
ApplyDateRange(defaultStart, defaultEnd, false);
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
await LoadAvailableCalendarsAsync();
|
||||
|
||||
var args = parameters as CalendarEventComposeNavigationArgs;
|
||||
ApplyNavigationArgs(args);
|
||||
UpdateRecurrenceSummary();
|
||||
}
|
||||
|
||||
partial void OnSelectedCalendarChanged(AccountCalendarViewModel value)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == value.DefaultShowAs)
|
||||
?? ShowAsOptions.FirstOrDefault();
|
||||
OnPropertyChanged(nameof(SelectedCalendarDisplayText));
|
||||
OnPropertyChanged(nameof(SelectedCalendarAccountText));
|
||||
}
|
||||
|
||||
partial void OnIsAllDayChanged(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
if (AllDayEndDate.Date <= StartDate.Date)
|
||||
{
|
||||
AllDayEndDate = StartDate.AddDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateRecurrenceSummary();
|
||||
}
|
||||
|
||||
partial void OnStartDateChanged(DateTimeOffset value)
|
||||
{
|
||||
if (IsAllDay && AllDayEndDate.Date <= value.Date)
|
||||
{
|
||||
AllDayEndDate = value.AddDays(1);
|
||||
}
|
||||
|
||||
if (IsRecurring && WeekdayOptions.All(option => !option.IsSelected))
|
||||
{
|
||||
SelectSingleWeekday(value.DayOfWeek);
|
||||
}
|
||||
|
||||
UpdateRecurrenceSummary();
|
||||
}
|
||||
|
||||
partial void OnStartTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
|
||||
partial void OnEndTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
|
||||
partial void OnAllDayEndDateChanged(DateTimeOffset value) => UpdateRecurrenceSummary();
|
||||
partial void OnIsRecurringChanged(bool value)
|
||||
{
|
||||
if (value && WeekdayOptions.All(option => !option.IsSelected))
|
||||
{
|
||||
SelectSingleWeekday(StartDate.DayOfWeek);
|
||||
}
|
||||
|
||||
UpdateRecurrenceSummary();
|
||||
}
|
||||
partial void OnSelectedRecurrenceIntervalChanged(int value) => UpdateRecurrenceSummary();
|
||||
partial void OnSelectedRecurrenceFrequencyOptionChanged(CalendarComposeFrequencyOption value) => UpdateRecurrenceSummary();
|
||||
partial void OnRecurrenceEndDateChanged(DateTimeOffset? value) => UpdateRecurrenceSummary();
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddAttachmentsAsync()
|
||||
{
|
||||
var pickedFiles = await _dialogService.PickFilesMetadataAsync("*");
|
||||
if (pickedFiles.Count == 0)
|
||||
return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
foreach (var file in pickedFiles)
|
||||
{
|
||||
if (Attachments.Any(existing => existing.FilePath.Equals(file.FullFilePath, StringComparison.OrdinalIgnoreCase)))
|
||||
continue;
|
||||
|
||||
Attachments.Add(new CalendarComposeAttachmentViewModel(file.FileName, file.FullFilePath, file.FileExtension, file.Size));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveAttachment(CalendarComposeAttachmentViewModel attachment)
|
||||
{
|
||||
if (attachment == null)
|
||||
return;
|
||||
|
||||
Attachments.Remove(attachment);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearRecurrenceEndDate()
|
||||
{
|
||||
RecurrenceEndDate = null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateAsync()
|
||||
{
|
||||
var uniqueAttendees = Attendees
|
||||
.GroupBy(attendee => attendee.Email, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.ToList();
|
||||
|
||||
var createdResult = await BuildResultAsync(uniqueAttendees);
|
||||
|
||||
try
|
||||
{
|
||||
_composeResultValidator.Validate(createdResult);
|
||||
}
|
||||
catch (CalendarEventComposeValidationException ex)
|
||||
{
|
||||
ShowValidationMessage(ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
LastCreatedResult = createdResult;
|
||||
|
||||
_navigationService.GoBack();
|
||||
}
|
||||
|
||||
public async Task<List<AccountContact>> SearchContactsAsync(string queryText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(queryText) || queryText.Length < 2)
|
||||
return [];
|
||||
|
||||
return await _contactService.GetAddressInformationAsync(queryText).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<CalendarComposeAttendeeViewModel> GetAttendeeAsync(string tokenText)
|
||||
{
|
||||
if (!EmailValidator.Validate(tokenText))
|
||||
return null;
|
||||
|
||||
var existing = Attendees.Any(attendee => attendee.Email.Equals(tokenText, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing)
|
||||
return null;
|
||||
|
||||
var info = await _contactService.GetAddressInformationByAddressAsync(tokenText).ConfigureAwait(false);
|
||||
if (info != null)
|
||||
{
|
||||
return CalendarComposeAttendeeViewModel.FromContact(info);
|
||||
}
|
||||
|
||||
return new CalendarComposeAttendeeViewModel(string.Empty, tokenText);
|
||||
}
|
||||
|
||||
public void AddAttendee(CalendarComposeAttendeeViewModel attendee)
|
||||
{
|
||||
if (Attendees.Any(existing => existing.Email.Equals(attendee.Email, StringComparison.OrdinalIgnoreCase)))
|
||||
return;
|
||||
|
||||
Attendees.Add(attendee);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveAttendee(CalendarComposeAttendeeViewModel attendee)
|
||||
{
|
||||
if (attendee == null)
|
||||
return;
|
||||
|
||||
Attendees.Remove(attendee);
|
||||
}
|
||||
|
||||
public void NotifyAddressExists()
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_ContactExistsTitle,
|
||||
Translator.Info_ContactExistsMessage,
|
||||
InfoBarMessageType.Warning);
|
||||
}
|
||||
|
||||
public void NotifyInvalidEmail(string address)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.Info_InvalidAddressTitle,
|
||||
string.Format(Translator.Info_InvalidAddressMessage, address),
|
||||
InfoBarMessageType.Warning);
|
||||
}
|
||||
|
||||
private async Task LoadAvailableCalendarsAsync()
|
||||
{
|
||||
var accountCalendars = new List<AccountCalendarViewModel>();
|
||||
var groupedCalendars = new List<GroupedAccountCalendarViewModel>();
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
||||
var viewModels = calendars
|
||||
.Select(calendar => new AccountCalendarViewModel(account, calendar))
|
||||
.ToList();
|
||||
|
||||
accountCalendars.AddRange(viewModels);
|
||||
|
||||
if (viewModels.Count > 0)
|
||||
{
|
||||
groupedCalendars.Add(new GroupedAccountCalendarViewModel(account, viewModels));
|
||||
}
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
AvailableCalendars.Clear();
|
||||
AvailableCalendarGroups.Clear();
|
||||
|
||||
foreach (var calendar in accountCalendars.OrderBy(calendar => calendar.Account.Name).ThenBy(calendar => calendar.Name))
|
||||
{
|
||||
AvailableCalendars.Add(calendar);
|
||||
}
|
||||
|
||||
foreach (var group in groupedCalendars.OrderBy(group => group.Account.Name))
|
||||
{
|
||||
AvailableCalendarGroups.Add(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplyNavigationArgs(CalendarEventComposeNavigationArgs args)
|
||||
{
|
||||
var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
|
||||
var startDate = args?.StartDate != default ? args!.StartDate : defaultStart;
|
||||
var endDate = args?.EndDate != default ? args!.EndDate : defaultEnd;
|
||||
var isAllDay = args?.IsAllDay ?? false;
|
||||
|
||||
Title = args?.Title ?? string.Empty;
|
||||
Location = args?.Location ?? string.Empty;
|
||||
|
||||
ApplyDateRange(startDate, endDate, isAllDay);
|
||||
|
||||
SelectedCalendar = ResolveSelectedCalendar(args?.SelectedCalendarId);
|
||||
if (SelectedCalendar != null)
|
||||
{
|
||||
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == SelectedCalendar.DefaultShowAs)
|
||||
?? SelectedShowAsOption
|
||||
?? ShowAsOptions.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private AccountCalendarViewModel ResolveSelectedCalendar(Guid? selectedCalendarId)
|
||||
{
|
||||
if (selectedCalendarId.HasValue)
|
||||
{
|
||||
var selectedCalendar = AvailableCalendars.FirstOrDefault(calendar => calendar.Id == selectedCalendarId.Value);
|
||||
if (selectedCalendar != null)
|
||||
return selectedCalendar;
|
||||
}
|
||||
|
||||
return AvailableCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AvailableCalendars.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void ApplyDateRange(DateTime startDate, DateTime endDate, bool isAllDay)
|
||||
{
|
||||
IsAllDay = isAllDay;
|
||||
StartDate = new DateTimeOffset(startDate.Date);
|
||||
StartTime = startDate.TimeOfDay;
|
||||
EndTime = endDate.TimeOfDay;
|
||||
AllDayEndDate = new DateTimeOffset((isAllDay ? endDate.Date : startDate.Date.AddDays(1)));
|
||||
}
|
||||
|
||||
private async Task<CalendarEventComposeResult> BuildResultAsync(List<CalendarComposeAttendeeViewModel> uniqueAttendees)
|
||||
{
|
||||
if (RecurrenceEndDate.HasValue && RecurrenceEndDate.Value.Date < StartDate.Date)
|
||||
{
|
||||
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidRecurrenceEnd);
|
||||
}
|
||||
|
||||
var htmlNotes = GetHtmlNotesAsync == null ? string.Empty : await GetHtmlNotesAsync();
|
||||
var effectiveStart = GetEffectiveStartDateTime();
|
||||
var effectiveEnd = GetEffectiveEndDateTime();
|
||||
|
||||
return new CalendarEventComposeResult
|
||||
{
|
||||
CalendarId = SelectedCalendar?.Id ?? Guid.Empty,
|
||||
AccountId = SelectedCalendar?.Account.Id ?? Guid.Empty,
|
||||
Title = Title.Trim(),
|
||||
Location = Location?.Trim() ?? string.Empty,
|
||||
HtmlNotes = htmlNotes,
|
||||
StartDate = effectiveStart,
|
||||
EndDate = effectiveEnd,
|
||||
IsAllDay = IsAllDay,
|
||||
TimeZoneId = TimeZoneInfo.Local.Id,
|
||||
ShowAs = SelectedShowAsOption?.ShowAs ?? SelectedCalendar?.DefaultShowAs ?? CalendarItemShowAs.Busy,
|
||||
SelectedReminders = BuildSelectedReminders(),
|
||||
Attendees = BuildAttendees(uniqueAttendees),
|
||||
Attachments = Attachments.Select(attachment => attachment.ToDraftModel()).ToList(),
|
||||
Recurrence = BuildRecurrenceRule(),
|
||||
RecurrenceSummary = RecurrenceSummary
|
||||
};
|
||||
}
|
||||
|
||||
private List<Reminder> BuildSelectedReminders()
|
||||
{
|
||||
if (SelectedReminderOption == null)
|
||||
return [];
|
||||
|
||||
return
|
||||
[
|
||||
new Reminder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = Guid.Empty,
|
||||
DurationInSeconds = SelectedReminderOption.Minutes * 60L,
|
||||
ReminderType = CalendarItemReminderType.Popup
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static List<CalendarEventAttendee> BuildAttendees(IEnumerable<CalendarComposeAttendeeViewModel> attendees)
|
||||
{
|
||||
return attendees
|
||||
.Select(attendee => new CalendarEventAttendee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = Guid.Empty,
|
||||
Name = attendee.HasDistinctDisplayName ? attendee.DisplayName : string.Empty,
|
||||
Email = attendee.Email,
|
||||
AttendenceStatus = AttendeeStatus.NeedsAction,
|
||||
IsOrganizer = false,
|
||||
ResolvedContact = attendee.ResolvedContact
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private ReminderOption GetDefaultReminderOption()
|
||||
{
|
||||
var reminderMinutes = Math.Max(1, _preferencesService.DefaultReminderDurationInSeconds / 60);
|
||||
return ReminderOptions.FirstOrDefault(option => option.Minutes == reminderMinutes)
|
||||
?? ReminderOptions.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void UpdateRecurrenceSummary()
|
||||
{
|
||||
var effectiveStart = GetEffectiveStartDateTime();
|
||||
var effectiveEnd = GetEffectiveEndDateTime();
|
||||
var timeSummary = IsAllDay
|
||||
? Translator.CalendarItemAllDay
|
||||
: string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
Translator.CalendarEventCompose_TimeRangeSummary,
|
||||
effectiveStart.ToString(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", CultureInfo.CurrentCulture),
|
||||
effectiveEnd.ToString(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", CultureInfo.CurrentCulture));
|
||||
|
||||
if (!IsRecurring)
|
||||
{
|
||||
RecurrenceSummary = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
Translator.CalendarEventCompose_SingleOccurrenceSummary,
|
||||
effectiveStart.ToString("dddd yyyy-MM-dd", CultureInfo.CurrentCulture),
|
||||
timeSummary);
|
||||
return;
|
||||
}
|
||||
|
||||
var frequencyLabel = SelectedRecurrenceFrequencyOption?.PluralLabel(SelectedRecurrenceInterval)
|
||||
?? Translator.CalendarEventCompose_FrequencyWeekPlural;
|
||||
|
||||
var selectedDays = WeekdayOptions
|
||||
.Where(option => option.IsSelected)
|
||||
.Select(option => option.FullDayName)
|
||||
.ToList();
|
||||
|
||||
var weekdaySummary = selectedDays.Count == 0
|
||||
? string.Empty
|
||||
: string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
Translator.CalendarEventCompose_WeekdaySummary,
|
||||
string.Join(", ", selectedDays));
|
||||
|
||||
var untilSummary = RecurrenceEndDate.HasValue
|
||||
? string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
Translator.CalendarEventCompose_UntilSummary,
|
||||
RecurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", CultureInfo.CurrentCulture))
|
||||
: string.Empty;
|
||||
|
||||
RecurrenceSummary = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
Translator.CalendarEventCompose_RecurringSummary,
|
||||
SelectedRecurrenceInterval,
|
||||
frequencyLabel,
|
||||
weekdaySummary,
|
||||
timeSummary,
|
||||
effectiveStart.ToString("dddd yyyy-MM-dd", CultureInfo.CurrentCulture),
|
||||
untilSummary).Trim();
|
||||
}
|
||||
|
||||
private string BuildRecurrenceRule()
|
||||
{
|
||||
if (!IsRecurring || SelectedRecurrenceFrequencyOption == null)
|
||||
return string.Empty;
|
||||
|
||||
var parts = new List<string>
|
||||
{
|
||||
$"FREQ={SelectedRecurrenceFrequencyOption.Frequency.ToString().ToUpperInvariant()}",
|
||||
$"INTERVAL={SelectedRecurrenceInterval}"
|
||||
};
|
||||
|
||||
var selectedDays = WeekdayOptions
|
||||
.Where(option => option.IsSelected)
|
||||
.Select(option => option.RuleValue)
|
||||
.ToList();
|
||||
|
||||
if (selectedDays.Count > 0)
|
||||
{
|
||||
parts.Add($"BYDAY={string.Join(",", selectedDays)}");
|
||||
}
|
||||
|
||||
if (RecurrenceEndDate.HasValue)
|
||||
{
|
||||
var untilValue = IsAllDay
|
||||
? RecurrenceEndDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture)
|
||||
: RecurrenceEndDate.Value.Date.AddDays(1).AddSeconds(-1).ToString("yyyyMMdd'T'HHmmss", CultureInfo.InvariantCulture);
|
||||
|
||||
parts.Add($"UNTIL={untilValue}");
|
||||
}
|
||||
|
||||
return $"RRULE:{string.Join(";", parts)}";
|
||||
}
|
||||
|
||||
private DateTime GetEffectiveStartDateTime()
|
||||
=> StartDate.Date.Add(IsAllDay ? TimeSpan.Zero : StartTime);
|
||||
|
||||
private DateTime GetEffectiveEndDateTime()
|
||||
=> IsAllDay
|
||||
? AllDayEndDate.Date
|
||||
: StartDate.Date.Add(EndTime);
|
||||
|
||||
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
|
||||
{
|
||||
var localNow = DateTime.Now;
|
||||
var roundedMinutes = localNow.Minute switch
|
||||
{
|
||||
< 30 => 30,
|
||||
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
|
||||
_ => 60
|
||||
};
|
||||
|
||||
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
|
||||
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
|
||||
|
||||
return (startDate, startDate.AddMinutes(30));
|
||||
}
|
||||
|
||||
private CalendarComposeWeekdayOption CreateWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
|
||||
{
|
||||
var option = new CalendarComposeWeekdayOption(dayOfWeek, ruleValue, label);
|
||||
option.PropertyChanged += WeekdayOptionPropertyChanged;
|
||||
return option;
|
||||
}
|
||||
|
||||
private void WeekdayOptionPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(CalendarComposeWeekdayOption.IsSelected))
|
||||
{
|
||||
UpdateRecurrenceSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectSingleWeekday(DayOfWeek dayOfWeek)
|
||||
{
|
||||
foreach (var option in WeekdayOptions)
|
||||
{
|
||||
option.IsSelected = option.DayOfWeek == dayOfWeek;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowValidationMessage(string message)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.CalendarEventCompose_ValidationTitle,
|
||||
message,
|
||||
InfoBarMessageType.Warning);
|
||||
}
|
||||
|
||||
private void AttachmentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasAttachments));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public partial class CalendarComposeFrequencyOption : ObservableObject
|
||||
{
|
||||
public CalendarItemRecurrenceFrequency Frequency { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency frequency, string displayText)
|
||||
{
|
||||
Frequency = frequency;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
|
||||
public string PluralLabel(int interval)
|
||||
{
|
||||
if (interval == 1)
|
||||
return DisplayText;
|
||||
|
||||
return Frequency switch
|
||||
{
|
||||
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
|
||||
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
|
||||
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
|
||||
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
|
||||
_ => DisplayText
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class CalendarComposeWeekdayOption : ObservableObject
|
||||
{
|
||||
public DayOfWeek DayOfWeek { get; }
|
||||
public string RuleValue { get; }
|
||||
public string Label { get; }
|
||||
public string FullDayName => DayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Monday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[1],
|
||||
DayOfWeek.Tuesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[2],
|
||||
DayOfWeek.Wednesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[3],
|
||||
DayOfWeek.Thursday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[4],
|
||||
DayOfWeek.Friday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[5],
|
||||
DayOfWeek.Saturday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[6],
|
||||
DayOfWeek.Sunday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[0],
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public CalendarComposeWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
|
||||
{
|
||||
DayOfWeek = dayOfWeek;
|
||||
RuleValue = ruleValue;
|
||||
Label = label;
|
||||
}
|
||||
}
|
||||
@@ -314,7 +314,44 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
|
||||
[RelayCommand]
|
||||
private void MoreDetails()
|
||||
{
|
||||
// TODO: Navigate to advanced event creation page with existing parameters.
|
||||
if (SelectedQuickEventDate == null)
|
||||
return;
|
||||
|
||||
var startDate = SelectedQuickEventDate.Value.Date.AddHours(9);
|
||||
var endDate = startDate.AddMinutes(30);
|
||||
|
||||
if (!IsAllDay)
|
||||
{
|
||||
var selectedStartTime = CurrentSettings.GetTimeSpan(SelectedStartTimeString);
|
||||
var selectedEndTime = CurrentSettings.GetTimeSpan(SelectedEndTimeString);
|
||||
|
||||
if (selectedStartTime.HasValue)
|
||||
{
|
||||
startDate = SelectedQuickEventDate.Value.Date.Add(selectedStartTime.Value);
|
||||
}
|
||||
|
||||
if (selectedEndTime.HasValue)
|
||||
{
|
||||
endDate = SelectedQuickEventDate.Value.Date.Add(selectedEndTime.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
startDate = SelectedQuickEventDate.Value.Date;
|
||||
endDate = SelectedQuickEventDate.Value.Date.AddDays(1);
|
||||
}
|
||||
|
||||
IsQuickEventDialogOpen = false;
|
||||
|
||||
_navigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
|
||||
{
|
||||
SelectedCalendarId = SelectedQuickEventAccountCalendar?.Id,
|
||||
Title = EventName ?? string.Empty,
|
||||
Location = Location ?? string.Empty,
|
||||
IsAllDay = IsAllDay,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate
|
||||
});
|
||||
}
|
||||
|
||||
public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Translations;
|
||||
using Wino.Core.ViewModels;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Calendar.ViewModels;
|
||||
|
||||
@@ -56,12 +53,21 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
[ObservableProperty]
|
||||
public partial int SelectedDefaultSnoozeIndex { get; set; }
|
||||
|
||||
public ObservableCollection<MailAccount> Accounts { get; } = [];
|
||||
public ObservableCollection<CalendarNewEventBehaviorOption> NewEventBehaviorOptions { get; } = [];
|
||||
public ObservableCollection<AccountCalendarViewModel> AvailableNewEventCalendars { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendarViewModel SelectedNewEventCalendar { get; set; }
|
||||
|
||||
public bool ShouldShowSpecificNewEventCalendar => SelectedNewEventBehaviorOption?.Behavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar;
|
||||
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public ObservableCollection<MailAccount> Accounts { get; } = new ObservableCollection<MailAccount>();
|
||||
|
||||
private readonly bool _isLoaded = false;
|
||||
|
||||
public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService, IAccountService accountService)
|
||||
@@ -71,10 +77,8 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
_accountService = accountService;
|
||||
|
||||
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
|
||||
|
||||
var cultureInfo = new CultureInfo(currentLanguageLanguageCode);
|
||||
|
||||
// Populate the day names list
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
DayNames.Add(cultureInfo.DateTimeFormat.DayNames[i]);
|
||||
@@ -89,7 +93,6 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
|
||||
WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
|
||||
|
||||
// Initialize reminder options
|
||||
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
|
||||
ReminderOptions.Add("None");
|
||||
foreach (var minutes in predefinedMinutes)
|
||||
@@ -102,10 +105,9 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
ReminderOptions.Add(displayText);
|
||||
}
|
||||
|
||||
// Set selected index based on current default reminder setting
|
||||
if (preferencesService.DefaultReminderDurationInSeconds == 0)
|
||||
{
|
||||
SelectedDefaultReminderIndex = 0; // None
|
||||
SelectedDefaultReminderIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -123,35 +125,47 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
var selectedSnoozeIndex = Array.IndexOf(supportedSnoozeMinutes, preferencesService.DefaultSnoozeDurationInMinutes);
|
||||
SelectedDefaultSnoozeIndex = selectedSnoozeIndex >= 0 ? selectedSnoozeIndex : 0;
|
||||
|
||||
NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AskEachTime, Translator.CalendarSettings_NewEventBehavior_AskEachTime));
|
||||
NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AlwaysUseSpecificCalendar, Translator.CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar));
|
||||
SelectedNewEventBehaviorOption = NewEventBehaviorOptions.FirstOrDefault(option => option.Behavior == preferencesService.NewEventButtonBehavior)
|
||||
?? NewEventBehaviorOptions.First();
|
||||
|
||||
_isLoaded = true;
|
||||
|
||||
// Load accounts with calendar support
|
||||
LoadAccountsAsync();
|
||||
}
|
||||
|
||||
private async void LoadAccountsAsync()
|
||||
{
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
var calendarsByAccount = new List<(MailAccount Account, List<AccountCalendarViewModel> Calendars)>();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
||||
calendarsByAccount.Add((account, calendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList()));
|
||||
}
|
||||
|
||||
await Dispatcher.ExecuteOnUIThread(() =>
|
||||
{
|
||||
Accounts.Clear();
|
||||
AvailableNewEventCalendars.Clear();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
Accounts.Add(account);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void NavigateToAccountSettings(MailAccount account)
|
||||
{
|
||||
if (account == null) return;
|
||||
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||
string.Format(Translator.CalendarAccountSettings_Description, account.Name),
|
||||
WinoPage.CalendarAccountSettingsPage,
|
||||
account.Id));
|
||||
foreach (var accountCalendars in calendarsByAccount)
|
||||
{
|
||||
foreach (var calendar in accountCalendars.Calendars)
|
||||
{
|
||||
AvailableNewEventCalendars.Add(calendar);
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStoredNewEventCalendarPreference();
|
||||
});
|
||||
}
|
||||
|
||||
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
|
||||
@@ -163,10 +177,17 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
|
||||
partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings();
|
||||
partial void OnSelectedDefaultSnoozeIndexChanged(int value) => SaveSettings();
|
||||
partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value)
|
||||
{
|
||||
OnPropertyChanged(nameof(ShouldShowSpecificNewEventCalendar));
|
||||
SaveSettings();
|
||||
}
|
||||
partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value) => SaveSettings();
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (!_isLoaded) return;
|
||||
if (!_isLoaded)
|
||||
return;
|
||||
|
||||
PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
|
||||
{
|
||||
@@ -209,10 +230,9 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
PreferencesService.WorkingHourEnd = WorkingHourEnd;
|
||||
PreferencesService.HourHeight = CellHourHeight;
|
||||
|
||||
// Save default reminder setting
|
||||
if (SelectedDefaultReminderIndex == 0)
|
||||
{
|
||||
PreferencesService.DefaultReminderDurationInSeconds = 0; // None
|
||||
PreferencesService.DefaultReminderDurationInSeconds = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -228,6 +248,47 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
PreferencesService.DefaultSnoozeDurationInMinutes = supportedSnoozeMinutes[selectedIndex];
|
||||
}
|
||||
|
||||
Messenger.Send(new CalendarSettingsUpdatedMessage());
|
||||
var newEventBehavior = SelectedNewEventBehaviorOption?.Behavior ?? NewEventButtonBehavior.AskEachTime;
|
||||
if (newEventBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && SelectedNewEventCalendar != null)
|
||||
{
|
||||
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AlwaysUseSpecificCalendar;
|
||||
PreferencesService.DefaultNewEventCalendarId = SelectedNewEventCalendar.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
|
||||
PreferencesService.DefaultNewEventCalendarId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyStoredNewEventCalendarPreference()
|
||||
{
|
||||
var configuredCalendarId = PreferencesService.DefaultNewEventCalendarId;
|
||||
var configuredCalendar = configuredCalendarId.HasValue
|
||||
? AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.Id == configuredCalendarId.Value)
|
||||
: null;
|
||||
|
||||
if (PreferencesService.NewEventButtonBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && configuredCalendar == null)
|
||||
{
|
||||
SelectedNewEventBehaviorOption = NewEventBehaviorOptions.First(option => option.Behavior == NewEventButtonBehavior.AskEachTime);
|
||||
SelectedNewEventCalendar = null;
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedNewEventCalendar = configuredCalendar
|
||||
?? AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.IsPrimary)
|
||||
?? AvailableNewEventCalendars.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CalendarNewEventBehaviorOption
|
||||
{
|
||||
public NewEventButtonBehavior Behavior { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public CalendarNewEventBehaviorOption(NewEventButtonBehavior behavior, string displayText)
|
||||
{
|
||||
Behavior = behavior;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Extensions;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Data;
|
||||
|
||||
public class CalendarComposeAttachmentViewModel
|
||||
{
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
public string FileName { get; }
|
||||
public string FilePath { get; }
|
||||
public string FileExtension { get; }
|
||||
public long Size { get; }
|
||||
public string ReadableSize => Size.GetBytesReadable();
|
||||
public MailAttachmentType AttachmentType { get; }
|
||||
|
||||
public CalendarComposeAttachmentViewModel(string fileName, string filePath, string fileExtension, long size)
|
||||
{
|
||||
FileName = fileName;
|
||||
FilePath = filePath;
|
||||
FileExtension = fileExtension;
|
||||
Size = size;
|
||||
AttachmentType = GetAttachmentType(fileExtension);
|
||||
}
|
||||
|
||||
public CalendarEventComposeAttachmentDraft ToDraftModel()
|
||||
{
|
||||
return new CalendarEventComposeAttachmentDraft
|
||||
{
|
||||
Id = Id,
|
||||
FileName = FileName,
|
||||
FilePath = FilePath,
|
||||
FileExtension = FileExtension,
|
||||
Size = Size
|
||||
};
|
||||
}
|
||||
|
||||
private static MailAttachmentType GetAttachmentType(string extension)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(extension))
|
||||
return MailAttachmentType.None;
|
||||
|
||||
return extension.ToLowerInvariant() switch
|
||||
{
|
||||
".exe" => MailAttachmentType.Executable,
|
||||
".rar" => MailAttachmentType.RarArchive,
|
||||
".zip" => MailAttachmentType.Archive,
|
||||
".ogg" or ".mp3" or ".wav" or ".aac" or ".alac" => MailAttachmentType.Audio,
|
||||
".mp4" or ".wmv" or ".avi" or ".flv" => MailAttachmentType.Video,
|
||||
".pdf" => MailAttachmentType.PDF,
|
||||
".htm" or ".html" => MailAttachmentType.HTML,
|
||||
".png" or ".jpg" or ".jpeg" or ".gif" or ".jiff" => MailAttachmentType.Image,
|
||||
_ => MailAttachmentType.Other
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Data;
|
||||
|
||||
public class CalendarComposeAttendeeViewModel
|
||||
{
|
||||
public string DisplayName { get; }
|
||||
public string Email { get; }
|
||||
public AccountContact ResolvedContact { get; }
|
||||
public bool HasDistinctDisplayName => !string.IsNullOrWhiteSpace(DisplayName) && !DisplayName.Equals(Email, System.StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public CalendarComposeAttendeeViewModel(string displayName, string email, AccountContact resolvedContact = null)
|
||||
{
|
||||
DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName;
|
||||
Email = email;
|
||||
ResolvedContact = resolvedContact;
|
||||
}
|
||||
|
||||
public static CalendarComposeAttendeeViewModel FromContact(AccountContact contact)
|
||||
=> new(contact.Name, contact.Address, contact);
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TimePeriodLibrary.NET" />
|
||||
<PackageReference Include="TimePeriodLibrary.NET" />
|
||||
<PackageReference Include="EmailValidation" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum NewEventButtonBehavior
|
||||
{
|
||||
AskEachTime = 0,
|
||||
AlwaysUseSpecificCalendar = 1
|
||||
}
|
||||
@@ -33,6 +33,7 @@ public enum WinoPage
|
||||
CalendarSettingsPage,
|
||||
CalendarAccountSettingsPage,
|
||||
EventDetailsPage,
|
||||
CalendarEventComposePage,
|
||||
SignatureAndEncryptionPage,
|
||||
StoragePage,
|
||||
WelcomePageV2,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Exceptions;
|
||||
|
||||
public sealed class CalendarEventComposeValidationException : Exception
|
||||
{
|
||||
public CalendarEventComposeValidationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public interface ICalendarService
|
||||
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
|
||||
Task SetPrimaryCalendarAsync(Guid accountId, Guid accountCalendarId);
|
||||
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,6 +29,7 @@ public interface IDialogServiceBase
|
||||
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
||||
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
|
||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
|
||||
Task<string> PickFilePathAsync(string saveFileName);
|
||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
@@ -18,6 +20,7 @@ public interface IMailDialogService : IDialogServiceBase
|
||||
// Custom dialogs
|
||||
Task<IMailItemFolder> ShowMoveMailFolderDialogAsync(List<IMailItemFolder> availableFolders);
|
||||
Task<MailAccount> ShowAccountPickerDialogAsync(List<MailAccount> availableAccounts);
|
||||
Task<AccountCalendar> ShowSingleCalendarPickerDialogAsync(List<CalendarPickerAccountGroup> availableCalendarGroups);
|
||||
|
||||
/// <summary>
|
||||
/// Displays a dialog to the user for reordering accounts.
|
||||
|
||||
@@ -227,6 +227,16 @@ public interface IPreferencesService : INotifyPropertyChanged
|
||||
/// </summary>
|
||||
int DefaultSnoozeDurationInMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setting: How the New Event button chooses a calendar.
|
||||
/// </summary>
|
||||
NewEventButtonBehavior NewEventButtonBehavior { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setting: Default calendar used when New Event is configured to always use a specific calendar.
|
||||
/// </summary>
|
||||
Guid? DefaultNewEventCalendarId { get; set; }
|
||||
|
||||
CalendarSettings GetCurrentCalendarSettings();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public class CalendarEventComposeAttachmentDraft
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string FileExtension { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public class CalendarEventComposeNavigationArgs
|
||||
{
|
||||
public Guid? SelectedCalendarId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public bool IsAllDay { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public class CalendarEventComposeResult
|
||||
{
|
||||
public Guid CalendarId { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public string HtmlNotes { get; set; } = string.Empty;
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public bool IsAllDay { get; set; }
|
||||
public string TimeZoneId { get; set; } = string.Empty;
|
||||
public CalendarItemShowAs ShowAs { get; set; }
|
||||
public List<Reminder> SelectedReminders { get; set; } = [];
|
||||
public List<CalendarEventAttendee> Attendees { get; set; } = [];
|
||||
public List<CalendarEventComposeAttachmentDraft> Attachments { get; set; } = [];
|
||||
public string Recurrence { get; set; } = string.Empty;
|
||||
public string RecurrenceSummary { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public class CalendarPickerAccountGroup
|
||||
{
|
||||
public MailAccount Account { get; set; } = null!;
|
||||
public List<AccountCalendar> Calendars { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Common;
|
||||
|
||||
public record PickedFileMetadata(string FullFilePath, long Size)
|
||||
{
|
||||
public string FileName => Path.GetFileName(FullFilePath);
|
||||
public string FileExtension => Path.GetExtension(FullFilePath)?.ToLowerInvariant() ?? string.Empty;
|
||||
}
|
||||
@@ -124,6 +124,56 @@
|
||||
"CalendarAttendeeStatus_NeedsAction": "Needs Action",
|
||||
"CalendarAttendeeStatus_Tentative": "Tentative",
|
||||
"CalendarEventDetails_Attachments": "Attachments",
|
||||
"CalendarEventCompose_AddAttachment": "Add attachment",
|
||||
"CalendarEventCompose_AllDay": "All Day",
|
||||
"CalendarEventCompose_EndDate": "End date",
|
||||
"CalendarEventCompose_EndTime": "End time",
|
||||
"CalendarEventCompose_Every": "every",
|
||||
"CalendarEventCompose_ForWeekdays": "for",
|
||||
"CalendarEventCompose_FrequencyDay": "day",
|
||||
"CalendarEventCompose_FrequencyDayPlural": "days",
|
||||
"CalendarEventCompose_FrequencyMonth": "month",
|
||||
"CalendarEventCompose_FrequencyMonthPlural": "months",
|
||||
"CalendarEventCompose_FrequencyWeek": "week",
|
||||
"CalendarEventCompose_FrequencyWeekPlural": "weeks",
|
||||
"CalendarEventCompose_FrequencyYear": "year",
|
||||
"CalendarEventCompose_FrequencyYearPlural": "years",
|
||||
"CalendarEventCompose_Location": "Location",
|
||||
"CalendarEventCompose_LocationPlaceholder": "Add a location",
|
||||
"CalendarEventCompose_NewEventButton": "New Event",
|
||||
"CalendarEventCompose_NoCalendarsMessage": "There are no calendars available for event creation yet.",
|
||||
"CalendarEventCompose_NoCalendarsTitle": "No calendars available",
|
||||
"CalendarEventCompose_NoEndDate": "No end date",
|
||||
"CalendarEventCompose_Notes": "Notes",
|
||||
"CalendarEventCompose_PickCalendarTitle": "Pick a calendar",
|
||||
"CalendarEventCompose_Recurring": "Recurring",
|
||||
"CalendarEventCompose_RecurringSummary": "Occurs every {0} {1}{2} {3} effective {4}{5}",
|
||||
"CalendarEventCompose_RepeatEvery": "Repeat every",
|
||||
"CalendarEventCompose_SelectCalendar": "Select calendar",
|
||||
"CalendarEventCompose_SingleOccurrenceSummary": "Occurs on {0} {1}",
|
||||
"CalendarEventCompose_StartDate": "Start date",
|
||||
"CalendarEventCompose_StartTime": "Start time",
|
||||
"CalendarEventCompose_TimeRangeSummary": "from {0} to {1}",
|
||||
"CalendarEventCompose_Title": "Event title",
|
||||
"CalendarEventCompose_TitlePlaceholder": "Add a title",
|
||||
"CalendarEventCompose_Until": "until",
|
||||
"CalendarEventCompose_UntilSummary": " until {0}",
|
||||
"CalendarEventCompose_ValidationInvalidAllDayRange": "The all-day end date must be after the start date.",
|
||||
"CalendarEventCompose_ValidationInvalidAttendee": "One or more attendees have an invalid email address.",
|
||||
"CalendarEventCompose_ValidationInvalidRecurrenceEnd": "The recurrence end date must be on or after the event start date.",
|
||||
"CalendarEventCompose_ValidationInvalidTimeRange": "The end time must be later than the start time.",
|
||||
"CalendarEventCompose_ValidationMissingAttachment": "One or more attachments are no longer available: {0}",
|
||||
"CalendarEventCompose_ValidationMissingCalendar": "Select a calendar before creating the event.",
|
||||
"CalendarEventCompose_ValidationMissingTitle": "Enter an event title before creating the event.",
|
||||
"CalendarEventCompose_ValidationTitle": "Event validation failed",
|
||||
"CalendarEventCompose_WeekdaySummary": " on {0}",
|
||||
"CalendarEventCompose_Weekday_Friday": "F",
|
||||
"CalendarEventCompose_Weekday_Monday": "M",
|
||||
"CalendarEventCompose_Weekday_Saturday": "S",
|
||||
"CalendarEventCompose_Weekday_Sunday": "S",
|
||||
"CalendarEventCompose_Weekday_Thursday": "T",
|
||||
"CalendarEventCompose_Weekday_Tuesday": "T",
|
||||
"CalendarEventCompose_Weekday_Wednesday": "W",
|
||||
"CalendarEventDetails_Details": "Details",
|
||||
"CalendarEventDetails_EditSeries": "Edit Series",
|
||||
"CalendarEventDetails_Editing": "Editing",
|
||||
@@ -999,6 +1049,10 @@
|
||||
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
|
||||
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
|
||||
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account",
|
||||
"CalendarSettings_NewEventBehavior_Header": "New Event button behavior",
|
||||
"CalendarSettings_NewEventBehavior_Description": "Choose whether the New Event button should ask for a calendar each time or always open a specific calendar.",
|
||||
"CalendarSettings_NewEventBehavior_AskEachTime": "Ask each time.",
|
||||
"CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar": "Always use specific calendar.",
|
||||
"WhatIsNew_GetStartedButton": "Get Started",
|
||||
"WhatIsNew_ContinueAnywayButton": "Continue anyway",
|
||||
"WhatIsNew_PreparingForNewVersionButton": "Preparing for new version...",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Core.Domain.Validation;
|
||||
|
||||
public sealed class CalendarEventComposeResultValidator
|
||||
{
|
||||
public void Validate(CalendarEventComposeResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (result.CalendarId == Guid.Empty)
|
||||
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationMissingCalendar);
|
||||
|
||||
if (result.AccountId == Guid.Empty)
|
||||
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationMissingCalendar);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.Title))
|
||||
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationMissingTitle);
|
||||
|
||||
if (result.EndDate <= result.StartDate)
|
||||
{
|
||||
var message = result.IsAllDay
|
||||
? Translator.CalendarEventCompose_ValidationInvalidAllDayRange
|
||||
: Translator.CalendarEventCompose_ValidationInvalidTimeRange;
|
||||
|
||||
throw new CalendarEventComposeValidationException(message);
|
||||
}
|
||||
|
||||
var missingAttachments = result.Attachments
|
||||
.Where(attachment => string.IsNullOrWhiteSpace(attachment.FilePath) || !File.Exists(attachment.FilePath))
|
||||
.Select(attachment => attachment.FileName)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (missingAttachments.Count > 0)
|
||||
{
|
||||
throw new CalendarEventComposeValidationException(
|
||||
string.Format(Translator.CalendarEventCompose_ValidationMissingAttachment, string.Join(", ", missingAttachments)));
|
||||
}
|
||||
|
||||
var invalidAttendee = result.Attendees
|
||||
.FirstOrDefault(attendee => string.IsNullOrWhiteSpace(attendee.Email) || !IsValidEmailAddress(attendee.Email.Trim()));
|
||||
|
||||
if (invalidAttendee != null)
|
||||
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidAttendee);
|
||||
|
||||
var duplicateAttendeeGroups = result.Attendees
|
||||
.Where(attendee => !string.IsNullOrWhiteSpace(attendee.Email))
|
||||
.GroupBy(attendee => attendee.Email.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(group => group.Count() > 1);
|
||||
|
||||
if (duplicateAttendeeGroups != null)
|
||||
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidAttendee);
|
||||
}
|
||||
|
||||
private static bool IsValidEmailAddress(string address)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsedAddress = new MailAddress(address);
|
||||
return parsedAddress.Address.Equals(address, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using FluentAssertions;
|
||||
using System.IO;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Wino.Core.Tests.Services;
|
||||
|
||||
public sealed class CalendarEventComposeResultValidatorTests
|
||||
{
|
||||
private readonly CalendarEventComposeResultValidator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenResultIsValid_DoesNotThrow()
|
||||
{
|
||||
var tempFilePath = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.Attachments.Add(new CalendarEventComposeAttachmentDraft
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FileName = Path.GetFileName(tempFilePath),
|
||||
FilePath = tempFilePath,
|
||||
FileExtension = ".tmp",
|
||||
Size = 12
|
||||
});
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenEndDateIsBeforeStartDate_ThrowsValidationException()
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.EndDate = result.StartDate.AddMinutes(-30);
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should()
|
||||
.Throw<CalendarEventComposeValidationException>()
|
||||
.WithMessage(Translator.CalendarEventCompose_ValidationInvalidTimeRange);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenAllDayEndDateMatchesStartDate_ThrowsValidationException()
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.IsAllDay = true;
|
||||
result.EndDate = result.StartDate;
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should()
|
||||
.Throw<CalendarEventComposeValidationException>()
|
||||
.WithMessage(Translator.CalendarEventCompose_ValidationInvalidAllDayRange);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenAttachmentDoesNotExist_ThrowsValidationException()
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.Attachments.Add(new CalendarEventComposeAttachmentDraft
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FileName = "missing.txt",
|
||||
FilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.txt"),
|
||||
FileExtension = ".txt",
|
||||
Size = 42
|
||||
});
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should()
|
||||
.Throw<CalendarEventComposeValidationException>()
|
||||
.WithMessage(string.Format(Translator.CalendarEventCompose_ValidationMissingAttachment, "missing.txt"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenAttendeeEmailIsInvalid_ThrowsValidationException()
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.Attendees.Add(new CalendarEventAttendee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = Guid.Empty,
|
||||
Email = "not-an-email"
|
||||
});
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should()
|
||||
.Throw<CalendarEventComposeValidationException>()
|
||||
.WithMessage(Translator.CalendarEventCompose_ValidationInvalidAttendee);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenAttendeeEmailIsDuplicated_ThrowsValidationException()
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.Attendees.Add(new CalendarEventAttendee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = Guid.Empty,
|
||||
Email = "person@example.com"
|
||||
});
|
||||
result.Attendees.Add(new CalendarEventAttendee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = Guid.Empty,
|
||||
Email = "PERSON@example.com"
|
||||
});
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should()
|
||||
.Throw<CalendarEventComposeValidationException>()
|
||||
.WithMessage(Translator.CalendarEventCompose_ValidationInvalidAttendee);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WhenCalendarIdIsMissing_ThrowsValidationException()
|
||||
{
|
||||
var result = CreateValidResult();
|
||||
result.CalendarId = Guid.Empty;
|
||||
|
||||
Action act = () => _validator.Validate(result);
|
||||
|
||||
act.Should()
|
||||
.Throw<CalendarEventComposeValidationException>()
|
||||
.WithMessage(Translator.CalendarEventCompose_ValidationMissingCalendar);
|
||||
}
|
||||
|
||||
private static CalendarEventComposeResult CreateValidResult()
|
||||
{
|
||||
return new CalendarEventComposeResult
|
||||
{
|
||||
CalendarId = Guid.NewGuid(),
|
||||
AccountId = Guid.NewGuid(),
|
||||
Title = "Design review",
|
||||
StartDate = new DateTime(2026, 3, 7, 13, 30, 0),
|
||||
EndDate = new DateTime(2026, 3, 7, 14, 0, 0),
|
||||
TimeZoneId = TimeZoneInfo.Local.Id,
|
||||
SelectedReminders =
|
||||
[
|
||||
new Reminder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DurationInSeconds = 900
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
@@ -33,6 +33,11 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
public MailAccount Account { get; set; }
|
||||
public ObservableCollection<IMailItemFolder> CurrentFolders { get; set; } = [];
|
||||
public ObservableCollection<AccountCalendar> AccountCalendars { get; set; } = [];
|
||||
public ObservableCollection<AccountCalendarSettingsItemViewModel> AccountCalendarSettingsItems { get; } = [];
|
||||
public ObservableCollection<AccountCalendarShowAsOption> ShowAsOptions { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendar SelectedPrimaryCalendar { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedTabIndex { get; set; } = 1; // Default to Mail tab
|
||||
@@ -70,6 +75,12 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
_folderService = folderService;
|
||||
_calendarService = calendarService;
|
||||
_statePersistanceService = statePersistanceService;
|
||||
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Free, Translator.CalendarShowAs_Free));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Tentative, Translator.CalendarShowAs_Tentative));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.Busy, Translator.CalendarShowAs_Busy));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.OutOfOffice, Translator.CalendarShowAs_OutOfOffice));
|
||||
ShowAsOptions.Add(new AccountCalendarShowAsOption(CalendarItemShowAs.WorkingElsewhere, Translator.CalendarShowAs_WorkingElsewhere));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -164,21 +175,41 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
private async Task LoadAccountCalendarsAsync()
|
||||
{
|
||||
var calendars = await _calendarService.GetAccountCalendarsAsync(Account.Id);
|
||||
|
||||
AccountCalendars.Clear();
|
||||
foreach (var calendar in calendars)
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
AccountCalendars.Add(calendar);
|
||||
}
|
||||
AccountCalendars.Clear();
|
||||
AccountCalendarSettingsItems.Clear();
|
||||
|
||||
foreach (var calendar in calendars)
|
||||
{
|
||||
AccountCalendars.Add(calendar);
|
||||
AccountCalendarSettingsItems.Add(new AccountCalendarSettingsItemViewModel(calendar, ShowAsOptions));
|
||||
}
|
||||
});
|
||||
|
||||
SelectedPrimaryCalendar = AccountCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AccountCalendars.FirstOrDefault();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CalendarItemClicked(AccountCalendar calendar)
|
||||
{
|
||||
if (calendar == null) return;
|
||||
public AccountCalendarShowAsOption GetShowAsOption(CalendarItemShowAs showAs)
|
||||
=> ShowAsOptions.FirstOrDefault(option => option.ShowAs == showAs) ?? ShowAsOptions.First();
|
||||
|
||||
// Navigate to calendar settings page with breadcrumb
|
||||
Messenger.Send(new BreadcrumbNavigationRequested(calendar.Name, WinoPage.CalendarAccountSettingsPage, calendar));
|
||||
public async Task UpdateCalendarSynchronizationAsync(AccountCalendar calendar, bool isEnabled)
|
||||
{
|
||||
if (calendar == null || calendar.IsSynchronizationEnabled == isEnabled)
|
||||
return;
|
||||
|
||||
calendar.IsSynchronizationEnabled = isEnabled;
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
public async Task UpdateCalendarDefaultShowAsAsync(AccountCalendar calendar, AccountCalendarShowAsOption option)
|
||||
{
|
||||
if (calendar == null || option == null || calendar.DefaultShowAs == option.ShowAs)
|
||||
return;
|
||||
|
||||
calendar.DefaultShowAs = option.ShowAs;
|
||||
await _calendarService.UpdateAccountCalendarAsync(calendar);
|
||||
}
|
||||
|
||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
@@ -209,6 +240,50 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled;
|
||||
await _accountService.UpdateAccountAsync(Account);
|
||||
break;
|
||||
case nameof(SelectedPrimaryCalendar) when SelectedPrimaryCalendar != null:
|
||||
foreach (var calendar in AccountCalendars)
|
||||
{
|
||||
calendar.IsPrimary = calendar.Id == SelectedPrimaryCalendar.Id;
|
||||
}
|
||||
|
||||
await _calendarService.SetPrimaryCalendarAsync(Account.Id, SelectedPrimaryCalendar.Id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AccountCalendarShowAsOption
|
||||
{
|
||||
public CalendarItemShowAs ShowAs { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public AccountCalendarShowAsOption(CalendarItemShowAs showAs, string displayText)
|
||||
{
|
||||
ShowAs = showAs;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class AccountCalendarSettingsItemViewModel : ObservableObject
|
||||
{
|
||||
public AccountCalendar Calendar { get; }
|
||||
public ObservableCollection<AccountCalendarShowAsOption> ShowAsOptions { get; }
|
||||
|
||||
public string Name => Calendar.Name;
|
||||
public string TimeZone => Calendar.TimeZone;
|
||||
public string BackgroundColorHex => Calendar.BackgroundColorHex;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSynchronizationEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AccountCalendarShowAsOption SelectedShowAsOption { get; set; }
|
||||
|
||||
public AccountCalendarSettingsItemViewModel(AccountCalendar calendar, ObservableCollection<AccountCalendarShowAsOption> showAsOptions)
|
||||
{
|
||||
Calendar = calendar;
|
||||
ShowAsOptions = showAsOptions;
|
||||
IsSynchronizationEnabled = calendar.IsSynchronizationEnabled;
|
||||
SelectedShowAsOption = showAsOptions.FirstOrDefault(option => option.ShowAs == calendar.DefaultShowAs) ?? showAsOptions.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ public partial class App : WinoApplication,
|
||||
services.AddTransient(typeof(CalendarSettingsPageViewModel));
|
||||
services.AddTransient(typeof(CalendarAccountSettingsPageViewModel));
|
||||
services.AddTransient(typeof(EventDetailsPageViewModel));
|
||||
services.AddTransient(typeof(CalendarEventComposePageViewModel));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -14,7 +14,6 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Helpers;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.WinUI;
|
||||
|
||||
@@ -176,7 +175,7 @@ public sealed partial class CalendarMailItemDisplayInformationControl : UserCont
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
calendarMimePart.Content.DecodeTo(stream);
|
||||
calendarMimePart.Content?.DecodeTo(stream);
|
||||
var contentBytes = stream.ToArray();
|
||||
|
||||
if (contentBytes.Length == 0)
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.WinUI;
|
||||
|
||||
@@ -30,6 +31,15 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
||||
[GeneratedDependencyProperty]
|
||||
public partial IMailItemDisplayInformation? MailItemInformation { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial AccountContact? PreviewContact { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial string? Address { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial string? DisplayNameOverride { get; set; }
|
||||
|
||||
private readonly IThumbnailService? _thumbnailService;
|
||||
private readonly IPreferencesService? _preferencesService;
|
||||
private readonly IContactPictureFileService? _contactPictureFileService;
|
||||
@@ -73,6 +83,11 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
||||
|
||||
RequestRefresh();
|
||||
}
|
||||
|
||||
partial void OnPreviewContactPropertyChanged(DependencyPropertyChangedEventArgs e) => RequestRefresh();
|
||||
partial void OnAddressPropertyChanged(DependencyPropertyChangedEventArgs e) => RequestRefresh();
|
||||
partial void OnDisplayNameOverridePropertyChanged(DependencyPropertyChangedEventArgs e) => RequestRefresh();
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RequestRefresh();
|
||||
@@ -262,7 +277,7 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
||||
var address = ResolveAddress();
|
||||
var displayName = ResolveDisplayName(address);
|
||||
var base64Picture = ResolveBase64Picture();
|
||||
var contactPictureFileId = MailItemInformation?.SenderContact?.ContactPictureFileId;
|
||||
var contactPictureFileId = PreviewContact?.ContactPictureFileId ?? MailItemInformation?.SenderContact?.ContactPictureFileId;
|
||||
|
||||
return new RefreshSnapshot(displayName, address, contactPictureFileId, base64Picture);
|
||||
}).ConfigureAwait(false);
|
||||
@@ -270,6 +285,12 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
||||
|
||||
private string ResolveAddress()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(PreviewContact?.Address))
|
||||
return PreviewContact.Address.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Address))
|
||||
return Address.Trim();
|
||||
|
||||
if (MailItemInformation == null)
|
||||
return string.Empty;
|
||||
|
||||
@@ -285,6 +306,12 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
||||
|
||||
private string ResolveDisplayName(string resolvedAddress)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(PreviewContact?.Name))
|
||||
return PreviewContact.Name.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(DisplayNameOverride))
|
||||
return DisplayNameOverride.Trim();
|
||||
|
||||
var contactName = MailItemInformation?.SenderContact?.Name;
|
||||
if (!string.IsNullOrWhiteSpace(contactName))
|
||||
return contactName.Trim();
|
||||
@@ -297,6 +324,9 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
||||
|
||||
private string ResolveBase64Picture()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(PreviewContact?.Base64ContactPicture))
|
||||
return PreviewContact.Base64ContactPicture;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(MailItemInformation?.SenderContact?.Base64ContactPicture))
|
||||
return MailItemInformation.SenderContact.Base64ContactPicture;
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<ContentDialog
|
||||
x:Class="Wino.Dialogs.SingleCalendarPickerDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:calendar="using:Wino.Core.Domain.Models.Calendar"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:sharedCalendar="using:Wino.Core.Domain.Entities.Calendar"
|
||||
Title="{x:Bind domain:Translator.CalendarEventCompose_PickCalendarTitle}"
|
||||
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
|
||||
Style="{StaticResource WinoDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMinWidth">420</x:Double>
|
||||
<Style
|
||||
x:Key="CalendarPickerListItemStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Margin" Value="0,2" />
|
||||
</Style>
|
||||
</ContentDialog.Resources>
|
||||
|
||||
<ScrollViewer Margin="0,8,0,0">
|
||||
<ItemsControl ItemsSource="{x:Bind AvailableGroups}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendar:CalendarPickerAccountGroup">
|
||||
<StackPanel Margin="0,0,0,12" Spacing="6">
|
||||
<TextBlock FontWeight="SemiBold">
|
||||
<Run Text="{x:Bind Account.Name}" />
|
||||
<Run Text=" (" />
|
||||
<Run Text="{x:Bind Account.Address}" />
|
||||
<Run Text=")" />
|
||||
</TextBlock>
|
||||
|
||||
<ListView
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CalendarClicked"
|
||||
ItemContainerStyle="{StaticResource CalendarPickerListItemStyle}"
|
||||
ItemsSource="{x:Bind Calendars}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="sharedCalendar:AccountCalendar">
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Ellipse
|
||||
Width="14"
|
||||
Height="14"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Dialogs;
|
||||
|
||||
public sealed partial class SingleCalendarPickerDialog : ContentDialog
|
||||
{
|
||||
public AccountCalendar? PickedCalendar { get; private set; }
|
||||
|
||||
public List<CalendarPickerAccountGroup> AvailableGroups { get; } = [];
|
||||
|
||||
public SingleCalendarPickerDialog(List<CalendarPickerAccountGroup> availableGroups)
|
||||
{
|
||||
AvailableGroups = availableGroups;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CalendarClicked(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
PickedCalendar = e.ClickedItem as AccountCalendar;
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,7 @@
|
||||
<coreControls:WinoFontIcon Icon="NewMail" />
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
<TextBlock
|
||||
Margin="0,-2,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Style="{StaticResource FlyoutPickerTitleTextBlockStyle}"
|
||||
|
||||
@@ -6,12 +6,14 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
@@ -122,6 +124,18 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
||||
return accountPicker.PickedAccount ?? null!;
|
||||
}
|
||||
|
||||
public async Task<AccountCalendar> ShowSingleCalendarPickerDialogAsync(List<CalendarPickerAccountGroup> availableCalendarGroups)
|
||||
{
|
||||
var calendarPicker = new SingleCalendarPickerDialog(availableCalendarGroups)
|
||||
{
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||
};
|
||||
|
||||
await HandleDialogPresentationAsync(calendarPicker);
|
||||
|
||||
return calendarPicker.PickedCalendar ?? null!;
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature? signatureModel = null)
|
||||
{
|
||||
SignatureEditorDialog signatureEditorDialog;
|
||||
|
||||
@@ -120,6 +120,40 @@ public class DialogServiceBase : IDialogServiceBase
|
||||
return returnList;
|
||||
}
|
||||
|
||||
public async Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters)
|
||||
{
|
||||
var returnList = new List<PickedFileMetadata>();
|
||||
var picker = new FileOpenPicker
|
||||
{
|
||||
ViewMode = PickerViewMode.Thumbnail,
|
||||
SuggestedStartLocation = PickerLocationId.Desktop
|
||||
};
|
||||
|
||||
foreach (var filter in typeFilters)
|
||||
{
|
||||
picker.FileTypeFilter.Add(filter.ToString());
|
||||
}
|
||||
|
||||
var mainWindow = WinoApplication.MainWindow;
|
||||
if (mainWindow == null) return returnList;
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(mainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var files = await picker.PickMultipleFilesAsync();
|
||||
if (files == null) return returnList;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
StorageApplicationPermissions.FutureAccessList.Add(file);
|
||||
|
||||
var basicProperties = await file.GetBasicPropertiesAsync();
|
||||
returnList.Add(new PickedFileMetadata(file.Path, (long)basicProperties.Size));
|
||||
}
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
private async Task<StorageFile?> PickFileAsync(params object[] typeFilters)
|
||||
{
|
||||
var picker = new FileOpenPicker
|
||||
|
||||
@@ -55,7 +55,8 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
private static readonly WinoPage[] CalendarOnlyPages =
|
||||
[
|
||||
WinoPage.CalendarPage,
|
||||
WinoPage.EventDetailsPage
|
||||
WinoPage.EventDetailsPage,
|
||||
WinoPage.CalendarEventComposePage
|
||||
];
|
||||
|
||||
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
|
||||
@@ -126,6 +127,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.SpecialImapCredentialsPage => typeof(SpecialImapCredentialsPage),
|
||||
WinoPage.CalendarPage => typeof(CalendarPage),
|
||||
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
|
||||
WinoPage.CalendarEventComposePage => typeof(CalendarEventComposePage),
|
||||
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
|
||||
WinoPage.CalendarAccountSettingsPage => typeof(CalendarAccountSettingsPage),
|
||||
_ => null,
|
||||
@@ -248,7 +250,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
}
|
||||
|
||||
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
|
||||
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
|
||||
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage || page == WinoPage.CalendarEventComposePage;
|
||||
|
||||
Frame? innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
||||
if (innerShellFrame == null && frame == NavigationReferenceFrame.ShellFrame)
|
||||
|
||||
@@ -290,6 +290,18 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
||||
set => SaveProperty(propertyName: nameof(DefaultSnoozeDurationInMinutes), value);
|
||||
}
|
||||
|
||||
public NewEventButtonBehavior NewEventButtonBehavior
|
||||
{
|
||||
get => _configurationService.Get(nameof(NewEventButtonBehavior), NewEventButtonBehavior.AskEachTime);
|
||||
set => SetPropertyAndSave(nameof(NewEventButtonBehavior), value);
|
||||
}
|
||||
|
||||
public Guid? DefaultNewEventCalendarId
|
||||
{
|
||||
get => _configurationService.Get<Guid?>(nameof(DefaultNewEventCalendarId), null);
|
||||
set => SetPropertyAndSave(nameof(DefaultNewEventCalendarId), value);
|
||||
}
|
||||
|
||||
public int EmailSyncIntervalMinutes
|
||||
{
|
||||
get => _configurationService.Get(nameof(EmailSyncIntervalMinutes), 3);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:WebViewEditorControl">
|
||||
<Grid CornerRadius="3">
|
||||
<Grid CornerRadius="{StaticResource ControlCornerRadius}">
|
||||
<Grid Background="White" Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsEditorDarkMode, Mode=OneWay, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" />
|
||||
<WebView2 x:Name="WebView" />
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Wino.Calendar.ViewModels;
|
||||
|
||||
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||
|
||||
public abstract class CalendarEventComposePageAbstract : BasePage<CalendarEventComposePageViewModel> { }
|
||||
@@ -14,6 +14,7 @@
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
|
||||
xmlns:local="using:Wino.Views"
|
||||
xmlns:mailViewModels="using:Wino.Mail.ViewModels"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
@@ -50,7 +51,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Checked="SyncFolderToggled"
|
||||
IsChecked="{x:Bind IsSynchronizationEnabled, Mode=OneWay}"
|
||||
Tag="{Binding}"
|
||||
Tag="{x:Bind}"
|
||||
Unchecked="SyncFolderToggled"
|
||||
Visibility="{x:Bind IsMoveTarget}" />
|
||||
|
||||
@@ -61,7 +62,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Checked="UnreadBadgeCheckboxToggled"
|
||||
IsChecked="{x:Bind ShowUnreadCount, Mode=OneWay}"
|
||||
Tag="{Binding}"
|
||||
Tag="{x:Bind}"
|
||||
Unchecked="UnreadBadgeCheckboxToggled"
|
||||
Visibility="{x:Bind IsMoveTarget}" />
|
||||
</Grid>
|
||||
@@ -296,27 +297,72 @@
|
||||
Visibility="Collapsed">
|
||||
<StackPanel MaxWidth="900" Spacing="12">
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarAccountSettings_PrimaryCalendarDescription, Mode=OneTime}" Header="{x:Bind domain:Translator.CalendarAccountSettings_PrimaryCalendar, Mode=OneTime}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ComboBox ItemsSource="{x:Bind ViewModel.AccountCalendars, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedPrimaryCalendar, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendar:AccountCalendar">
|
||||
<TextBlock Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Calendars List -->
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.AccountCalendars, Mode=OneWay}">
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.AccountCalendarSettingsItems, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendar:AccountCalendar">
|
||||
<controls:SettingsCard
|
||||
Margin="0,0,0,4"
|
||||
ActionIcon="{ui:FontIcon Glyph=}"
|
||||
Click="CalendarItemClicked"
|
||||
CommandParameter="{x:Bind}"
|
||||
<DataTemplate x:DataType="mailViewModels:AccountCalendarSettingsItemViewModel">
|
||||
<controls:SettingsExpander
|
||||
Margin="0,0,0,8"
|
||||
Description="{x:Bind TimeZone, Mode=OneWay}"
|
||||
Header="{x:Bind Name, Mode=OneWay}"
|
||||
IsClickEnabled="True">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<Border
|
||||
Width="32"
|
||||
Height="32"
|
||||
Background="{x:Bind BackgroundColorHex, Mode=OneWay}"
|
||||
CornerRadius="4" />
|
||||
</controls:SettingsCard>
|
||||
IsExpanded="False">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
<SymbolIcon Foreground="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}" Symbol="Calendar" />
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
<controls:SettingsExpander.Items>
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarAccountSettings_AccountColorDescription, Mode=OneTime}" Header="{x:Bind domain:Translator.CalendarAccountSettings_AccountColor, Mode=OneTime}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<Border
|
||||
Width="32"
|
||||
Height="32"
|
||||
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}"
|
||||
CornerRadius="4" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarAccountSettings_SyncEnabledDescription, Mode=OneTime}" Header="{x:Bind domain:Translator.CalendarAccountSettings_SyncEnabled, Mode=OneTime}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ToggleSwitch
|
||||
IsOn="{x:Bind IsSynchronizationEnabled, Mode=OneWay}"
|
||||
Tag="{x:Bind}"
|
||||
Toggled="CalendarSynchronizationToggled" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarAccountSettings_DefaultShowAsDescription, Mode=OneTime}" Header="{x:Bind domain:Translator.CalendarAccountSettings_DefaultShowAs, Mode=OneTime}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ComboBox
|
||||
ItemsSource="{x:Bind ShowAsOptions, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind SelectedShowAsOption, Mode=OneWay}"
|
||||
SelectionChanged="CalendarShowAsSelectionChanged"
|
||||
Tag="{x:Bind}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="mailViewModels:AccountCalendarShowAsOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Mail.ViewModels;
|
||||
using Wino.Views.Abstract;
|
||||
|
||||
namespace Wino.Views;
|
||||
@@ -37,11 +37,21 @@ public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract
|
||||
}
|
||||
}
|
||||
|
||||
private void CalendarItemClicked(object sender, RoutedEventArgs e)
|
||||
private async void CalendarSynchronizationToggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is CommunityToolkit.WinUI.Controls.SettingsCard settingsCard && settingsCard.CommandParameter is AccountCalendar calendar)
|
||||
if (sender is ToggleSwitch { Tag: AccountCalendarSettingsItemViewModel calendarItem } toggleSwitch)
|
||||
{
|
||||
ViewModel.CalendarItemClickedCommand?.Execute(calendar);
|
||||
calendarItem.IsSynchronizationEnabled = toggleSwitch.IsOn;
|
||||
await ViewModel.UpdateCalendarSynchronizationAsync(calendarItem.Calendar, toggleSwitch.IsOn);
|
||||
}
|
||||
}
|
||||
|
||||
private async void CalendarShowAsSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is ComboBox { Tag: AccountCalendarSettingsItemViewModel calendarItem, SelectedItem: AccountCalendarShowAsOption option })
|
||||
{
|
||||
calendarItem.SelectedShowAsOption = option;
|
||||
await ViewModel.UpdateCalendarDefaultShowAsAsync(calendarItem.Calendar, option);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
@@ -56,16 +57,6 @@
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsSyncEnabled, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Primary Calendar -->
|
||||
<controls:SettingsCard
|
||||
Description="{x:Bind domain:Translator.CalendarAccountSettings_PrimaryCalendarDescription, Mode=OneTime}"
|
||||
Header="{x:Bind domain:Translator.CalendarAccountSettings_PrimaryCalendar, Mode=OneTime}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsPrimaryCalendar, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Default Show As Status -->
|
||||
<controls:SettingsCard
|
||||
Description="{x:Bind domain:Translator.CalendarAccountSettings_DefaultShowAsDescription, Mode=OneTime}"
|
||||
@@ -75,9 +66,14 @@
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ComboBox
|
||||
MinWidth="150"
|
||||
DisplayMemberPath="DisplayText"
|
||||
ItemsSource="{x:Bind ViewModel.ShowAsOptions, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedDefaultShowAsOption, Mode=TwoWay}" />
|
||||
SelectedItem="{x:Bind ViewModel.SelectedDefaultShowAsOption, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:ShowAsOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -82,10 +82,10 @@
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
Background="Transparent"
|
||||
DisplayType="{x:Bind ViewModel.StatePersistenceService.CalendarDisplayType, Mode=OneWay}"
|
||||
FontSize="14"
|
||||
FontWeight="Normal"
|
||||
IsHitTestVisible="False"
|
||||
DisplayType="{x:Bind ViewModel.StatePersistenceService.CalendarDisplayType, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.DateNavigationHeaderItems}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedDateNavigationHeaderIndex, Mode=OneWay}">
|
||||
<FlipView.ItemTemplate>
|
||||
@@ -149,14 +149,37 @@
|
||||
<SplitView.Pane>
|
||||
<Grid Padding="0,0,0,6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Button
|
||||
Margin="10,12,10,0"
|
||||
Padding="4,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
Command="{x:Bind ViewModel.NewEventCommand}">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="16" Icon="NewMail" />
|
||||
<TextBlock
|
||||
Margin="10,-2,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Style="{StaticResource FlyoutPickerTitleTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.CalendarEventCompose_NewEventButton}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<calendarControls:WinoCalendarView
|
||||
x:Name="CalendarView"
|
||||
Grid.Row="0"
|
||||
Grid.Row="1"
|
||||
Margin="0,12,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
DateClickedCommand="{x:Bind ViewModel.DateClickedCommand}"
|
||||
@@ -167,7 +190,7 @@
|
||||
<!-- Account Calendars Host -->
|
||||
<ListView
|
||||
x:Name="CalendarHostListView"
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
ItemsSource="{x:Bind ViewModel.AccountCalendarStateService.GroupedAccountCalendars}"
|
||||
SelectionMode="None">
|
||||
<ListView.Header>
|
||||
@@ -261,13 +284,16 @@
|
||||
|
||||
<!-- Menu Items -->
|
||||
<ListView
|
||||
Grid.Row="2"
|
||||
Grid.Row="3"
|
||||
ItemTemplateSelector="{StaticResource NavigationMenuTemplateSelector}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedMenuItemIndex, Mode=TwoWay}">
|
||||
<ListView.Items>
|
||||
<ListViewItem>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" FontSize="16" Glyph="" />
|
||||
<FontIcon
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.MenuManageAccounts}" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<abstract:CalendarEventComposePageAbstract
|
||||
x:Class="Wino.Calendar.Views.CalendarEventComposePage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
|
||||
xmlns:controls="using:Wino.Controls"
|
||||
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
|
||||
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:mailControls="using:Wino.Mail.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shared="using:Wino.Core.Domain.Entities.Shared"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
Style="{StaticResource PageStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<Style
|
||||
x:Key="TransparentActionButtonStyle"
|
||||
BasedOn="{StaticResource DefaultButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="8" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="FieldToggleButtonStyle"
|
||||
BasedOn="{StaticResource DefaultToggleButtonStyle}"
|
||||
TargetType="ToggleButton">
|
||||
<Setter Property="Padding" Value="12,6" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="AttendeeSuggestionTemplate" x:DataType="shared:AccountContact">
|
||||
<StackPanel Padding="8,4" Orientation="Vertical">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
|
||||
<TextBlock FontSize="12" Text="{x:Bind Address}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="AttendeeTokenTemplate" x:DataType="data:CalendarComposeAttendeeViewModel">
|
||||
<TextBlock Text="{x:Bind DisplayName}" />
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Top Bar -->
|
||||
<Grid
|
||||
Padding="16,10"
|
||||
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left: Save + Discard -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Command="{x:Bind ViewModel.CreateCommand}">
|
||||
<Button.Style>
|
||||
<Style BasedOn="{StaticResource AccentButtonStyle}" TargetType="Button">
|
||||
<Setter Property="Padding" Value="16,6" />
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="16" Icon="Save" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Save}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Command="{x:Bind ViewModel.CancelCommand}" Style="{StaticResource TransparentActionButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="16" Icon="Dismiss" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Discard}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right: Calendar, Show As, Reminder -->
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12">
|
||||
|
||||
<!-- Calendar -->
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="Calendar" />
|
||||
<Button
|
||||
Padding="10,6"
|
||||
HorizontalContentAlignment="Left"
|
||||
Style="{StaticResource DefaultButtonStyle}">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="BottomEdgeAlignedLeft">
|
||||
<ScrollViewer MaxHeight="360">
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.AvailableCalendarGroups, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:GroupedAccountCalendarViewModel">
|
||||
<StackPanel
|
||||
MinWidth="320"
|
||||
Padding="0,0,0,12"
|
||||
Spacing="6">
|
||||
<TextBlock FontWeight="SemiBold">
|
||||
<Run Text="{x:Bind Account.Name}" />
|
||||
<Run Text=" (" />
|
||||
<Run Text="{x:Bind Account.Address}" />
|
||||
<Run Text=")" />
|
||||
</TextBlock>
|
||||
|
||||
<ListView
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ComposeCalendarClicked"
|
||||
ItemsSource="{x:Bind AccountCalendars}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:AccountCalendarViewModel">
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Ellipse
|
||||
Width="14"
|
||||
Height="14"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Ellipse
|
||||
Width="10"
|
||||
Height="10"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(ViewModel.SelectedCalendar.BackgroundColorHex), Mode=OneWay}" />
|
||||
<StackPanel Spacing="0">
|
||||
<TextBlock Text="{x:Bind ViewModel.SelectedCalendarDisplayText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.SelectedCalendarAccountText, Mode=OneWay}"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.StringToVisibilityConverter(ViewModel.SelectedCalendarAccountText), Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<FontIcon Glyph="" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Show As -->
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="CalendarShowAs" />
|
||||
<ComboBox
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind ViewModel.ShowAsOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedShowAsOption, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:ShowAsOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Reminder -->
|
||||
<StackPanel
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="Reminder" />
|
||||
<ComboBox
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind ViewModel.ReminderOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedReminderOption, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:ReminderOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<ScrollViewer Grid.Row="1" MaxWidth="1200">
|
||||
<StackPanel
|
||||
Padding="40,24,40,40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="0">
|
||||
|
||||
<!-- Title -->
|
||||
<TextBox
|
||||
FontSize="24"
|
||||
PlaceholderText="{x:Bind domain:Translator.CalendarEventCompose_TitlePlaceholder}"
|
||||
Text="{x:Bind ViewModel.Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<TextBox.Style>
|
||||
<Style BasedOn="{StaticResource DefaultTextBoxStyle}" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<!--<Setter Property="Padding" Value="0,8" />-->
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
|
||||
<!-- Divider -->
|
||||
<Border
|
||||
Height="1"
|
||||
Margin="0,0,0,20"
|
||||
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<!-- Date Row -->
|
||||
<StackPanel
|
||||
Margin="0,0,0,12"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12">
|
||||
<DatePicker Date="{x:Bind ViewModel.StartDate, Mode=TwoWay}" Header="{x:Bind domain:Translator.CalendarEventCompose_StartDate}" />
|
||||
|
||||
<ToggleButton
|
||||
VerticalAlignment="Bottom"
|
||||
Content="{x:Bind domain:Translator.CalendarEventCompose_AllDay}"
|
||||
IsChecked="{x:Bind ViewModel.IsAllDay, Mode=TwoWay}"
|
||||
Style="{StaticResource FieldToggleButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Time Row -->
|
||||
<StackPanel
|
||||
Margin="0,0,0,8"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsAllDay), Mode=OneWay}">
|
||||
<TimePicker
|
||||
ClockIdentifier="{x:Bind ViewModel.TimePickerClockIdentifier, Mode=OneWay}"
|
||||
Header="{x:Bind domain:Translator.CalendarEventCompose_StartTime}"
|
||||
SelectedTime="{x:Bind ViewModel.StartTime, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="0,28,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="—" />
|
||||
|
||||
<TimePicker
|
||||
ClockIdentifier="{x:Bind ViewModel.TimePickerClockIdentifier, Mode=OneWay}"
|
||||
Header="{x:Bind domain:Translator.CalendarEventCompose_EndTime}"
|
||||
SelectedTime="{x:Bind ViewModel.EndTime, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- All Day End Date -->
|
||||
<DatePicker
|
||||
Margin="0,0,0,8"
|
||||
Date="{x:Bind ViewModel.AllDayEndDate, Mode=TwoWay}"
|
||||
Header="{x:Bind domain:Translator.CalendarEventCompose_EndDate}"
|
||||
Visibility="{x:Bind ViewModel.IsAllDay, Mode=OneWay}" />
|
||||
|
||||
<!-- Date/Time Summary -->
|
||||
<TextBlock
|
||||
Margin="0,0,0,16"
|
||||
FontSize="13"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.RecurrenceSummary, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<!-- Location -->
|
||||
<TextBox
|
||||
Margin="0,0,0,12"
|
||||
PlaceholderText="{x:Bind domain:Translator.CalendarEventCompose_LocationPlaceholder}"
|
||||
Text="{x:Bind ViewModel.Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<!-- Recurring Toggle -->
|
||||
<ToggleButton
|
||||
Margin="0,0,0,4"
|
||||
IsChecked="{x:Bind ViewModel.IsRecurring, Mode=TwoWay}"
|
||||
Style="{StaticResource FieldToggleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="CalendarEventRepeat" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.CalendarEventCompose_Recurring}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
|
||||
<!-- Recurrence Panel -->
|
||||
<Border
|
||||
Margin="0,8,0,4"
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind ViewModel.IsRecurring, Mode=OneWay}">
|
||||
<StackPanel Spacing="12">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.CalendarEventCompose_RepeatEvery}" />
|
||||
<ComboBox
|
||||
Grid.Column="1"
|
||||
ItemsSource="{x:Bind ViewModel.RecurrenceIntervalOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedRecurrenceInterval, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind domain:Translator.CalendarEventCompose_Every}" />
|
||||
<ComboBox
|
||||
Grid.Column="3"
|
||||
ItemsSource="{x:Bind ViewModel.RecurrenceFrequencyOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedRecurrenceFrequencyOption, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:CalendarComposeFrequencyOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind domain:Translator.CalendarEventCompose_ForWeekdays}" />
|
||||
<ItemsControl Grid.Column="5" ItemsSource="{x:Bind ViewModel.WeekdayOptions}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:CalendarComposeWeekdayOption">
|
||||
<ToggleButton
|
||||
Width="32"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
IsChecked="{x:Bind IsSelected, Mode=TwoWay}">
|
||||
<TextBlock HorizontalAlignment="Center" Text="{x:Bind Label}" />
|
||||
</ToggleButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock
|
||||
Grid.Column="6"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind domain:Translator.CalendarEventCompose_Until}" />
|
||||
<CalendarDatePicker
|
||||
Grid.Column="7"
|
||||
Date="{x:Bind ViewModel.RecurrenceEndDate, Mode=TwoWay}"
|
||||
PlaceholderText="{x:Bind domain:Translator.CalendarEventCompose_NoEndDate}" />
|
||||
<Button
|
||||
Grid.Column="8"
|
||||
Command="{x:Bind ViewModel.ClearRecurrenceEndDateCommand}"
|
||||
Style="{StaticResource TransparentActionButtonStyle}">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="Dismiss" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- People & Attachments Toggle Row -->
|
||||
<StackPanel
|
||||
Margin="0,12,0,4"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<ToggleButton x:Name="PeopleToggle" Style="{StaticResource FieldToggleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="People" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.CalendarEventDetails_People}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
|
||||
<ToggleButton x:Name="AttachmentsToggle" Style="{StaticResource FieldToggleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="Attachment" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.CalendarEventDetails_Attachments}" />
|
||||
</StackPanel>
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
|
||||
<!-- People Pane -->
|
||||
<Border
|
||||
Margin="0,8,0,0"
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind PeopleToggle.IsChecked, Mode=OneWay}">
|
||||
<StackPanel Spacing="8">
|
||||
<toolkit:TokenizingTextBox
|
||||
x:Name="AttendeeBox"
|
||||
BorderThickness="0"
|
||||
ItemsSource="{x:Bind ViewModel.Attendees, Mode=OneWay}"
|
||||
LostFocus="AddressBoxLostFocus"
|
||||
PlaceholderText="{x:Bind domain:Translator.CalendarEventDetails_InviteSomeone}"
|
||||
SuggestedItemTemplate="{StaticResource AttendeeSuggestionTemplate}"
|
||||
TokenDelimiter=";"
|
||||
TokenItemAdding="TokenItemAdding"
|
||||
TokenItemTemplate="{StaticResource AttendeeTokenTemplate}" />
|
||||
|
||||
<ListView
|
||||
Margin="-8,0,-8,-8"
|
||||
ItemsSource="{x:Bind ViewModel.Attendees, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:CalendarComposeAttendeeViewModel">
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:ImagePreviewControl
|
||||
Width="32"
|
||||
Height="32"
|
||||
Address="{x:Bind Email}"
|
||||
DisplayNameOverride="{x:Bind DisplayName}"
|
||||
PreviewContact="{x:Bind ResolvedContact}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="1">
|
||||
<TextBlock
|
||||
FontSize="13"
|
||||
Text="{x:Bind DisplayName}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Email}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Visibility="{x:Bind HasDistinctDisplayName, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Padding="6,4"
|
||||
VerticalAlignment="Center"
|
||||
Click="RemoveAttendeeClicked"
|
||||
Style="{StaticResource TransparentActionButtonStyle}"
|
||||
Tag="{x:Bind}">
|
||||
<coreControls:WinoFontIcon FontSize="12" Icon="Delete" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Attachments Pane -->
|
||||
<Border
|
||||
Margin="0,8,0,0"
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind AttachmentsToggle.IsChecked, Mode=OneWay}">
|
||||
<StackPanel Spacing="8">
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Command="{x:Bind ViewModel.AddAttachmentsCommand}"
|
||||
Style="{StaticResource TransparentActionButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="14" Icon="AttachmentNew" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.CalendarEventCompose_AddAttachment}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<ListView
|
||||
Margin="-8,0,-8,-8"
|
||||
ItemsSource="{x:Bind ViewModel.Attachments, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:CalendarComposeAttachmentViewModel">
|
||||
<Grid Height="44" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentControl
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind AttachmentType}"
|
||||
ContentTemplateSelector="{StaticResource FileTypeIconSelector}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="1">
|
||||
<TextBlock
|
||||
FontSize="13"
|
||||
Text="{x:Bind FileName}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ReadableSize}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Padding="6,4"
|
||||
VerticalAlignment="Center"
|
||||
Click="RemoveAttachmentClicked"
|
||||
Style="{StaticResource TransparentActionButtonStyle}"
|
||||
Tag="{x:Bind}">
|
||||
<coreControls:WinoFontIcon FontSize="12" Icon="Delete" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Notes Header -->
|
||||
<StackPanel
|
||||
Margin="0,20,0,8"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<coreControls:WinoFontIcon
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Icon="Draft" />
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.CalendarEventCompose_Notes}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Notes Editor -->
|
||||
<mailControls:WebViewEditorControl
|
||||
x:Name="NotesEditor"
|
||||
MinHeight="600"
|
||||
IsEditorDarkMode="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=OneWay}"
|
||||
IsEditorWebViewEditor="True" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</abstract:CalendarEventComposePageAbstract>
|
||||
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using EmailValidation;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Mail.WinUI.Views.Abstract;
|
||||
|
||||
namespace Wino.Calendar.Views;
|
||||
|
||||
public sealed partial class CalendarEventComposePage : CalendarEventComposePageAbstract,
|
||||
IRecipient<ApplicationThemeChanged>
|
||||
{
|
||||
private readonly List<IDisposable> _disposables = [];
|
||||
|
||||
public CalendarEventComposePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
_disposables.Add(GetSuggestionBoxDisposable(AttendeeBox));
|
||||
_disposables.Add(NotesEditor);
|
||||
|
||||
ViewModel.GetHtmlNotesAsync = async () => await NotesEditor.GetHtmlBodyAsync() ?? string.Empty;
|
||||
await NotesEditor.RenderHtmlAsync(" ");
|
||||
}
|
||||
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
base.OnNavigatingFrom(e);
|
||||
|
||||
foreach (var disposable in _disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_disposables.Clear();
|
||||
}
|
||||
|
||||
private IDisposable GetSuggestionBoxDisposable(TokenizingTextBox box)
|
||||
{
|
||||
return Observable.FromEventPattern<TypedEventHandler<AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>, AutoSuggestBoxTextChangedEventArgs>(
|
||||
handler => box.TextChanged += handler,
|
||||
handler => box.TextChanged -= handler)
|
||||
.Throttle(TimeSpan.FromMilliseconds(120))
|
||||
.ObserveOn(SynchronizationContext.Current!)
|
||||
.Subscribe(async eventPattern =>
|
||||
{
|
||||
if (eventPattern.EventArgs.Reason != AutoSuggestionBoxTextChangeReason.UserInput)
|
||||
return;
|
||||
|
||||
if (eventPattern.Sender is not AutoSuggestBox senderBox || senderBox.Text.Length < 2)
|
||||
return;
|
||||
|
||||
var addresses = await ViewModel.SearchContactsAsync(senderBox.Text).ConfigureAwait(false);
|
||||
await ViewModel.ExecuteUIThread(() => senderBox.ItemsSource = addresses);
|
||||
});
|
||||
}
|
||||
|
||||
private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
|
||||
{
|
||||
if (!EmailValidator.Validate(args.TokenText))
|
||||
{
|
||||
args.Cancel = true;
|
||||
ViewModel.NotifyInvalidEmail(args.TokenText);
|
||||
return;
|
||||
}
|
||||
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
try
|
||||
{
|
||||
var attendee = await ViewModel.GetAttendeeAsync(args.TokenText);
|
||||
if (attendee == null)
|
||||
{
|
||||
args.Cancel = true;
|
||||
ViewModel.NotifyAddressExists();
|
||||
return;
|
||||
}
|
||||
|
||||
args.Item = attendee;
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddressBoxLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not TokenizingTextBox tokenizingTextBox)
|
||||
return;
|
||||
|
||||
if (tokenizingTextBox.Items.LastOrDefault() is not ITokenStringContainer info)
|
||||
return;
|
||||
|
||||
var currentText = info.Text;
|
||||
if (string.IsNullOrWhiteSpace(currentText) || !EmailValidator.Validate(currentText))
|
||||
return;
|
||||
|
||||
var attendee = await ViewModel.GetAttendeeAsync(currentText);
|
||||
if (attendee == null)
|
||||
{
|
||||
tokenizingTextBox.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.AddAttendee(attendee);
|
||||
tokenizingTextBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void RemoveAttendeeClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button { Tag: CalendarComposeAttendeeViewModel attendee })
|
||||
{
|
||||
ViewModel.RemoveAttendeeCommand.Execute(attendee);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAttachmentClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button { Tag: CalendarComposeAttachmentViewModel attachment })
|
||||
{
|
||||
ViewModel.RemoveAttachmentCommand.Execute(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
private void ComposeCalendarClicked(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is AccountCalendarViewModel calendar)
|
||||
{
|
||||
ViewModel.SelectedCalendar = calendar;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ApplicationThemeChanged message)
|
||||
{
|
||||
ViewModel.IsDarkWebviewRenderer = message.IsUnderlyingThemeDark;
|
||||
NotesEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark;
|
||||
}
|
||||
|
||||
protected override void RegisterRecipients()
|
||||
{
|
||||
base.RegisterRecipients();
|
||||
WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this);
|
||||
}
|
||||
|
||||
protected override void UnregisterRecipients()
|
||||
{
|
||||
base.UnregisterRecipients();
|
||||
WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract"
|
||||
xmlns:calendarViewModels="using:Wino.Calendar.ViewModels"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:controls1="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Wino.Calendar.ViewModels.Data"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:entities="using:Wino.Core.Domain.Entities.Shared"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -236,9 +238,7 @@
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard
|
||||
Description="{x:Bind domain:Translator.CalendarSettings_DefaultSnoozeDuration_Description}"
|
||||
Header="{x:Bind domain:Translator.CalendarSettings_DefaultSnoozeDuration_Header}">
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarSettings_DefaultSnoozeDuration_Description}" Header="{x:Bind domain:Translator.CalendarSettings_DefaultSnoozeDuration_Header}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<PathIcon Data="F1 M 10 1.25 C 10.456706 1.25 10.889486 1.337565 11.298339 1.512695 C 11.707192 1.687826 12.072591 1.927409 12.394531 2.231445 C 12.716471 2.535482 12.97656 2.892253 13.173828 3.301758 C 13.371096 3.711263 13.481771 4.146484 13.505859 4.607422 L 13.505859 5 C 13.969401 5.028646 14.386068 5.16276 14.755859 5.402344 C 15.125651 5.641927 15.414713 5.947917 15.623047 6.320312 C 15.83138 6.692709 15.9349 7.096355 15.933594 7.53125 L 15.933594 8.75 L 16.25 8.75 C 16.822917 8.75 17.317057 8.95638 17.732422 9.369141 C 18.147787 9.781901 18.357422 10.273437 18.359375 10.84375 L 18.359375 16.40625 C 18.359375 16.979167 18.153971 17.473308 17.743164 17.888672 C 17.332357 18.304037 16.839844 18.511719 16.265625 18.511719 L 3.734375 18.511719 C 3.164062 18.511719 2.671549 18.304037 2.256836 17.888672 C 1.842122 17.473308 1.634114 16.979167 1.630859 16.40625 L 1.630859 10.84375 C 1.630859 10.273437 1.837565 9.781901 2.250977 9.369141 C 2.664388 8.95638 3.156901 8.75 3.728516 8.75 L 4.0625 8.75 L 4.0625 7.53125 C 4.0625 7.09375 4.166341 6.689453 4.374023 6.314453 C 4.581706 5.939453 4.86914 5.632486 5.236328 5.393555 C 5.603516 5.154623 6.019532 5.021485 6.484375 4.994141 L 6.484375 4.607422 C 6.506511 4.148438 6.617838 3.714518 6.818359 3.305664 C 7.01888 2.896811 7.282877 2.539063 7.610352 2.232422 C 7.937826 1.92578 8.308268 1.685222 8.721679 1.510742 C 9.135091 1.336264 9.56575 1.249024 10.013672 1.249024 Z M 10.013672 2.5 C 9.441406 2.5 8.947916 2.706381 8.533203 3.119141 C 8.118489 3.531901 7.911459 4.023437 7.912109 4.59375 L 7.912109 6.25 L 12.089844 6.25 L 12.089844 4.59375 C 12.089844 4.023438 11.882161 3.531902 11.466797 3.119141 C 11.051433 2.706382 10.557292 2.5 9.984375 2.5 Z M 5.3125 7.53125 L 5.3125 8.75 L 14.6875 8.75 L 14.6875 7.53125 C 14.6875 7.303385 14.605144 7.107747 14.440429 6.944336 C 14.275714 6.780925 14.080729 6.69987 13.855469 6.703125 L 6.142578 6.703125 C 5.914713 6.703125 5.71875 6.785482 5.552734 6.950195 C 5.386719 7.114909 5.30339 7.310547 5.300781 7.537109 Z M 16.25 10 L 3.75 10 C 3.540365 10 3.361328 10.071615 3.212891 10.214844 C 3.064453 10.358073 2.989258 10.534505 2.986328 10.744141 L 2.986328 16.40625 C 2.986328 16.618489 3.058595 16.797526 3.203125 16.943359 C 3.347656 17.089193 3.526693 17.161458 3.740234 17.15625 L 16.25 17.15625 C 16.458333 17.15625 16.636067 17.082683 16.783203 16.935547 C 16.930339 16.788411 17.003906 16.609375 17.003906 16.398437 L 17.003906 10.742187 C 17.003906 10.536459 16.932942 10.359049 16.791016 10.209961 C 16.64909 10.060873 16.469401 9.986328 16.257812 9.986328 Z M 10 11.5625 C 10.227865 11.5625 10.423502 11.644856 10.586914 11.809571 C 10.750325 11.974285 10.83138 12.16862 10.830078 12.393555 L 10.830078 13.427734 L 11.855469 13.427734 C 12.080404 13.427734 12.273437 13.509115 12.434571 13.671875 C 12.595704 13.834636 12.675781 14.026693 12.675781 14.248047 C 12.675781 14.46224 12.596354 14.653321 12.4375 14.813477 C 12.278646 14.973633 12.089192 15.053711 11.869141 15.053711 L 10.830078 15.053711 L 10.830078 16.074219 C 10.830078 16.296224 10.748372 16.486981 10.584961 16.646484 C 10.42155 16.805989 10.229818 16.885742 10.005859 16.885742 C 9.781901 16.885742 9.590495 16.806315 9.431641 16.647461 C 9.272786 16.488607 9.193359 16.296875 9.193359 16.072266 L 9.193359 15.053711 L 8.15625 15.053711 C 7.931314 15.053711 7.738934 14.972331 7.579102 14.80957 C 7.419269 14.646809 7.339192 14.454753 7.338867 14.233399 C 7.338867 14.008464 7.419921 13.815755 7.582031 13.655274 C 7.744141 13.494793 7.935872 13.414551 8.157227 13.414551 L 9.193359 13.414551 L 9.193359 12.394531 C 9.193359 12.166667 9.275715 11.971029 9.440429 11.807618 C 9.605144 11.644206 9.799479 11.562825 10.023437 11.5625 Z" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
@@ -246,6 +246,40 @@
|
||||
<ComboBox ItemsSource="{x:Bind ViewModel.SnoozeOptions, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SelectedDefaultSnoozeIndex, Mode=TwoWay}" />
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard Description="{x:Bind domain:Translator.CalendarSettings_NewEventBehavior_Description}" Header="{x:Bind domain:Translator.CalendarSettings_NewEventBehavior_Header}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<controls:SettingsCard.Content>
|
||||
<StackPanel Spacing="12">
|
||||
<ComboBox ItemsSource="{x:Bind ViewModel.NewEventBehaviorOptions, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedNewEventBehaviorOption, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:CalendarNewEventBehaviorOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<ComboBox
|
||||
ItemsSource="{x:Bind ViewModel.AvailableNewEventCalendars, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedNewEventCalendar, Mode=TwoWay}"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowSpecificNewEventCalendar, Mode=OneWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:AccountCalendarViewModel">
|
||||
<StackPanel Orientation="Vertical" Spacing="2">
|
||||
<TextBlock Text="{x:Bind Name}" />
|
||||
<TextBlock
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Account.Address}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
</StackPanel>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="ClockIdentifierStates">
|
||||
|
||||
@@ -179,9 +179,14 @@
|
||||
<ComboBox
|
||||
Width="150"
|
||||
VerticalAlignment="Center"
|
||||
DisplayMemberPath="DisplayText"
|
||||
ItemsSource="{x:Bind ViewModel.ShowAsOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedShowAsOption, Mode=TwoWay}" />
|
||||
SelectedItem="{x:Bind ViewModel.SelectedShowAsOption, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="calendarViewModels:ShowAsOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Reminder -->
|
||||
|
||||
@@ -54,11 +54,41 @@ public class CalendarService : BaseDatabaseService, ICalendarService
|
||||
|
||||
public async Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
if (accountCalendar.IsPrimary)
|
||||
{
|
||||
await Connection.ExecuteAsync(
|
||||
"UPDATE AccountCalendar SET IsPrimary = 0 WHERE AccountId = ? AND Id != ?",
|
||||
accountCalendar.AccountId,
|
||||
accountCalendar.Id);
|
||||
}
|
||||
|
||||
await Connection.UpdateAsync(accountCalendar, typeof(AccountCalendar));
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListUpdated(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task SetPrimaryCalendarAsync(Guid accountId, Guid accountCalendarId)
|
||||
{
|
||||
await Connection.RunInTransactionAsync(connection =>
|
||||
{
|
||||
connection.Execute(
|
||||
"UPDATE AccountCalendar SET IsPrimary = 0 WHERE AccountId = ?",
|
||||
accountId);
|
||||
|
||||
connection.Execute(
|
||||
"UPDATE AccountCalendar SET IsPrimary = 1 WHERE AccountId = ? AND Id = ?",
|
||||
accountId,
|
||||
accountCalendarId);
|
||||
});
|
||||
|
||||
var calendars = await GetAccountCalendarsAsync(accountId).ConfigureAwait(false);
|
||||
|
||||
foreach (var calendar in calendars)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListUpdated(calendar));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
await Connection.ExecuteAsync(
|
||||
|
||||
Reference in New Issue
Block a user