Initial event composing.
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
|
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
|
||||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
|
<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="morelinq" Version="4.4.0" />
|
||||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageVersion Include="Nito.AsyncEx.Tasks" 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="FluentAssertions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -9,6 +10,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Serilog;
|
using Serilog;
|
||||||
using Wino.Calendar.ViewModels.Data;
|
using Wino.Calendar.ViewModels.Data;
|
||||||
using Wino.Calendar.ViewModels.Interfaces;
|
using Wino.Calendar.ViewModels.Interfaces;
|
||||||
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Collections;
|
using Wino.Core.Domain.Collections;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Extensions;
|
using Wino.Core.Domain.Extensions;
|
||||||
@@ -73,7 +75,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
ICalendarService calendarService,
|
ICalendarService calendarService,
|
||||||
IAccountCalendarStateService accountCalendarStateService,
|
IAccountCalendarStateService accountCalendarStateService,
|
||||||
INavigationService navigationService,
|
INavigationService navigationService,
|
||||||
IDialogServiceBase dialogService,
|
IMailDialogService dialogService,
|
||||||
IUpdateManager updateManager)
|
IUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
@@ -290,7 +292,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
private DateTime? _navigationDate;
|
private DateTime? _navigationDate;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly ICalendarService _calendarService;
|
private readonly ICalendarService _calendarService;
|
||||||
private readonly IDialogServiceBase _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly IUpdateManager _updateManager;
|
private readonly IUpdateManager _updateManager;
|
||||||
|
|
||||||
#region Commands
|
#region Commands
|
||||||
@@ -306,6 +308,41 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
|
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task NewEventAsync()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var 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]
|
[RelayCommand]
|
||||||
@@ -436,4 +473,20 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
|
|
||||||
public async void Receive(AccountRemovedMessage message)
|
public async void Receive(AccountRemovedMessage message)
|
||||||
=> await InitializeAccountCalendarsAsync();
|
=> await InitializeAccountCalendarsAsync();
|
||||||
|
|
||||||
|
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,746 @@
|
|||||||
|
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.Net.Mail;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
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.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public Func<Task<string>> GetHtmlNotesAsync { get; set; }
|
||||||
|
|
||||||
|
public ObservableCollection<AccountCalendarViewModel> AvailableCalendars { 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 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 || SelectedShowAsOption != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == value.DefaultShowAs)
|
||||||
|
?? ShowAsOptions.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (!await ValidateAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var uniqueAttendees = Attendees
|
||||||
|
.GroupBy(attendee => attendee.Email, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => group.First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var htmlNotes = GetHtmlNotesAsync == null ? string.Empty : await GetHtmlNotesAsync();
|
||||||
|
var effectiveStart = GetEffectiveStartDateTime();
|
||||||
|
var effectiveEnd = GetEffectiveEndDateTime();
|
||||||
|
|
||||||
|
LastCreatedResult = new CalendarEventComposeResult
|
||||||
|
{
|
||||||
|
CalendarId = SelectedCalendar!.Id,
|
||||||
|
AccountId = SelectedCalendar.Account.Id,
|
||||||
|
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,
|
||||||
|
SelectedReminders = BuildSelectedReminders(),
|
||||||
|
Attendees = BuildAttendees(uniqueAttendees),
|
||||||
|
Attachments = Attachments.Select(attachment => attachment.ToDraftModel()).ToList(),
|
||||||
|
Recurrence = BuildRecurrenceRule(),
|
||||||
|
RecurrenceSummary = RecurrenceSummary
|
||||||
|
};
|
||||||
|
|
||||||
|
_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 (!IsValidEmailAddress(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 accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var account in accounts)
|
||||||
|
{
|
||||||
|
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var calendar in calendars)
|
||||||
|
{
|
||||||
|
accountCalendars.Add(new AccountCalendarViewModel(account, calendar));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
AvailableCalendars.Clear();
|
||||||
|
|
||||||
|
foreach (var calendar in accountCalendars.OrderBy(calendar => calendar.Account.Name).ThenBy(calendar => calendar.Name))
|
||||||
|
{
|
||||||
|
AvailableCalendars.Add(calendar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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<bool> ValidateAsync()
|
||||||
|
{
|
||||||
|
if (SelectedCalendar == null)
|
||||||
|
{
|
||||||
|
ShowValidationMessage(Translator.CalendarEventCompose_ValidationMissingCalendar);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Title))
|
||||||
|
{
|
||||||
|
ShowValidationMessage(Translator.CalendarEventCompose_ValidationMissingTitle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAllDay)
|
||||||
|
{
|
||||||
|
if (AllDayEndDate.Date <= StartDate.Date)
|
||||||
|
{
|
||||||
|
ShowValidationMessage(Translator.CalendarEventCompose_ValidationInvalidAllDayRange);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (GetEffectiveEndDateTime() <= GetEffectiveStartDateTime())
|
||||||
|
{
|
||||||
|
ShowValidationMessage(Translator.CalendarEventCompose_ValidationInvalidTimeRange);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RecurrenceEndDate.HasValue && RecurrenceEndDate.Value.Date < StartDate.Date)
|
||||||
|
{
|
||||||
|
ShowValidationMessage(Translator.CalendarEventCompose_ValidationInvalidRecurrenceEnd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingAttachments = Attachments
|
||||||
|
.Where(attachment => !File.Exists(attachment.FilePath))
|
||||||
|
.Select(attachment => attachment.FileName)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (missingAttachments.Count > 0)
|
||||||
|
{
|
||||||
|
ShowValidationMessage(string.Format(Translator.CalendarEventCompose_ValidationMissingAttachment, string.Join(", ", missingAttachments)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedAttendees = Attendees
|
||||||
|
.Where(attendee => !string.IsNullOrWhiteSpace(attendee.Email))
|
||||||
|
.Select(attendee => attendee.Email.Trim())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (normalizedAttendees.Any(address => !IsValidEmailAddress(address)))
|
||||||
|
{
|
||||||
|
ShowValidationMessage(Translator.CalendarEventCompose_ValidationInvalidAttendee);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetHtmlNotesAsync != null)
|
||||||
|
{
|
||||||
|
await GetHtmlNotesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidEmailAddress(string address)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsedAddress = new MailAddress(address);
|
||||||
|
return parsedAddress.Address.Equals(address, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
[RelayCommand]
|
||||||
private void MoreDetails()
|
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)
|
public void SelectQuickEventTimeRange(TimeSpan startTime, TimeSpan endTime)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ public enum WinoPage
|
|||||||
CalendarSettingsPage,
|
CalendarSettingsPage,
|
||||||
CalendarAccountSettingsPage,
|
CalendarAccountSettingsPage,
|
||||||
EventDetailsPage,
|
EventDetailsPage,
|
||||||
|
CalendarEventComposePage,
|
||||||
SignatureAndEncryptionPage,
|
SignatureAndEncryptionPage,
|
||||||
StoragePage,
|
StoragePage,
|
||||||
WelcomePageV2,
|
WelcomePageV2,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public interface IDialogServiceBase
|
|||||||
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
|
||||||
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
|
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
|
||||||
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
|
||||||
|
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
|
||||||
Task<string> PickFilePathAsync(string saveFileName);
|
Task<string> PickFilePathAsync(string saveFileName);
|
||||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Models;
|
using Wino.Core.Domain.Models;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
@@ -18,6 +20,7 @@ public interface IMailDialogService : IDialogServiceBase
|
|||||||
// Custom dialogs
|
// Custom dialogs
|
||||||
Task<IMailItemFolder> ShowMoveMailFolderDialogAsync(List<IMailItemFolder> availableFolders);
|
Task<IMailItemFolder> ShowMoveMailFolderDialogAsync(List<IMailItemFolder> availableFolders);
|
||||||
Task<MailAccount> ShowAccountPickerDialogAsync(List<MailAccount> availableAccounts);
|
Task<MailAccount> ShowAccountPickerDialogAsync(List<MailAccount> availableAccounts);
|
||||||
|
Task<AccountCalendar> ShowSingleCalendarPickerDialogAsync(List<CalendarPickerAccountGroup> availableCalendarGroups);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Displays a dialog to the user for reordering accounts.
|
/// Displays a dialog to the user for reordering accounts.
|
||||||
|
|||||||
@@ -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_NeedsAction": "Needs Action",
|
||||||
"CalendarAttendeeStatus_Tentative": "Tentative",
|
"CalendarAttendeeStatus_Tentative": "Tentative",
|
||||||
"CalendarEventDetails_Attachments": "Attachments",
|
"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_Details": "Details",
|
||||||
"CalendarEventDetails_EditSeries": "Edit Series",
|
"CalendarEventDetails_EditSeries": "Edit Series",
|
||||||
"CalendarEventDetails_Editing": "Editing",
|
"CalendarEventDetails_Editing": "Editing",
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ public partial class App : WinoApplication,
|
|||||||
services.AddTransient(typeof(CalendarSettingsPageViewModel));
|
services.AddTransient(typeof(CalendarSettingsPageViewModel));
|
||||||
services.AddTransient(typeof(CalendarAccountSettingsPageViewModel));
|
services.AddTransient(typeof(CalendarAccountSettingsPageViewModel));
|
||||||
services.AddTransient(typeof(EventDetailsPageViewModel));
|
services.AddTransient(typeof(EventDetailsPageViewModel));
|
||||||
|
services.AddTransient(typeof(CalendarEventComposePageViewModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ using Wino.Core.Domain;
|
|||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Helpers;
|
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Mail.WinUI;
|
using Wino.Mail.WinUI;
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ public sealed partial class CalendarMailItemDisplayInformationControl : UserCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
calendarMimePart.Content.DecodeTo(stream);
|
calendarMimePart.Content?.DecodeTo(stream);
|
||||||
var contentBytes = stream.ToArray();
|
var contentBytes = stream.ToArray();
|
||||||
|
|
||||||
if (contentBytes.Length == 0)
|
if (contentBytes.Length == 0)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Mail.WinUI;
|
using Wino.Mail.WinUI;
|
||||||
|
|
||||||
@@ -30,6 +31,15 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
|||||||
[GeneratedDependencyProperty]
|
[GeneratedDependencyProperty]
|
||||||
public partial IMailItemDisplayInformation? MailItemInformation { get; set; }
|
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 IThumbnailService? _thumbnailService;
|
||||||
private readonly IPreferencesService? _preferencesService;
|
private readonly IPreferencesService? _preferencesService;
|
||||||
private readonly IContactPictureFileService? _contactPictureFileService;
|
private readonly IContactPictureFileService? _contactPictureFileService;
|
||||||
@@ -73,6 +83,11 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
|||||||
|
|
||||||
RequestRefresh();
|
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)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
RequestRefresh();
|
RequestRefresh();
|
||||||
@@ -262,7 +277,7 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
|||||||
var address = ResolveAddress();
|
var address = ResolveAddress();
|
||||||
var displayName = ResolveDisplayName(address);
|
var displayName = ResolveDisplayName(address);
|
||||||
var base64Picture = ResolveBase64Picture();
|
var base64Picture = ResolveBase64Picture();
|
||||||
var contactPictureFileId = MailItemInformation?.SenderContact?.ContactPictureFileId;
|
var contactPictureFileId = PreviewContact?.ContactPictureFileId ?? MailItemInformation?.SenderContact?.ContactPictureFileId;
|
||||||
|
|
||||||
return new RefreshSnapshot(displayName, address, contactPictureFileId, base64Picture);
|
return new RefreshSnapshot(displayName, address, contactPictureFileId, base64Picture);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
@@ -270,6 +285,12 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
|||||||
|
|
||||||
private string ResolveAddress()
|
private string ResolveAddress()
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(PreviewContact?.Address))
|
||||||
|
return PreviewContact.Address.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Address))
|
||||||
|
return Address.Trim();
|
||||||
|
|
||||||
if (MailItemInformation == null)
|
if (MailItemInformation == null)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
@@ -285,6 +306,12 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
|||||||
|
|
||||||
private string ResolveDisplayName(string resolvedAddress)
|
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;
|
var contactName = MailItemInformation?.SenderContact?.Name;
|
||||||
if (!string.IsNullOrWhiteSpace(contactName))
|
if (!string.IsNullOrWhiteSpace(contactName))
|
||||||
return contactName.Trim();
|
return contactName.Trim();
|
||||||
@@ -297,6 +324,9 @@ public sealed partial class ImagePreviewControl : PersonPicture
|
|||||||
|
|
||||||
private string ResolveBase64Picture()
|
private string ResolveBase64Picture()
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(PreviewContact?.Base64ContactPicture))
|
||||||
|
return PreviewContact.Base64ContactPicture;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MailItemInformation?.SenderContact?.Base64ContactPicture))
|
if (!string.IsNullOrWhiteSpace(MailItemInformation?.SenderContact?.Base64ContactPicture))
|
||||||
return 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>
|
||||||
|
<StackPanel Margin="0,0,0,12" Spacing="6">
|
||||||
|
<TextBlock FontWeight="SemiBold">
|
||||||
|
<Run Text="{Binding Account.Name}" />
|
||||||
|
<Run Text=" (" />
|
||||||
|
<Run Text="{Binding Account.Address}" />
|
||||||
|
<Run Text=")" />
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
IsItemClickEnabled="True"
|
||||||
|
ItemClick="CalendarClicked"
|
||||||
|
ItemContainerStyle="{StaticResource CalendarPickerListItemStyle}"
|
||||||
|
ItemsSource="{Binding Calendars}"
|
||||||
|
SelectionMode="None">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid ColumnSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Ellipse
|
||||||
|
Width="14"
|
||||||
|
Height="14"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Fill="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,14 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Entities.Calendar;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models;
|
using Wino.Core.Domain.Models;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Mail.WinUI.Extensions;
|
using Wino.Mail.WinUI.Extensions;
|
||||||
@@ -122,6 +124,18 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
return accountPicker.PickedAccount ?? null!;
|
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)
|
public async Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature? signatureModel = null)
|
||||||
{
|
{
|
||||||
SignatureEditorDialog signatureEditorDialog;
|
SignatureEditorDialog signatureEditorDialog;
|
||||||
|
|||||||
@@ -120,6 +120,40 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
return returnList;
|
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)
|
private async Task<StorageFile?> PickFileAsync(params object[] typeFilters)
|
||||||
{
|
{
|
||||||
var picker = new FileOpenPicker
|
var picker = new FileOpenPicker
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
private static readonly WinoPage[] CalendarOnlyPages =
|
private static readonly WinoPage[] CalendarOnlyPages =
|
||||||
[
|
[
|
||||||
WinoPage.CalendarPage,
|
WinoPage.CalendarPage,
|
||||||
WinoPage.EventDetailsPage
|
WinoPage.EventDetailsPage,
|
||||||
|
WinoPage.CalendarEventComposePage
|
||||||
];
|
];
|
||||||
|
|
||||||
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
|
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
|
||||||
@@ -126,6 +127,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
WinoPage.SpecialImapCredentialsPage => typeof(SpecialImapCredentialsPage),
|
WinoPage.SpecialImapCredentialsPage => typeof(SpecialImapCredentialsPage),
|
||||||
WinoPage.CalendarPage => typeof(CalendarPage),
|
WinoPage.CalendarPage => typeof(CalendarPage),
|
||||||
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
|
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
|
||||||
|
WinoPage.CalendarEventComposePage => typeof(CalendarEventComposePage),
|
||||||
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
|
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
|
||||||
WinoPage.CalendarAccountSettingsPage => typeof(CalendarAccountSettingsPage),
|
WinoPage.CalendarAccountSettingsPage => typeof(CalendarAccountSettingsPage),
|
||||||
_ => null,
|
_ => null,
|
||||||
@@ -248,7 +250,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
|
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
|
||||||
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
|
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage || page == WinoPage.CalendarEventComposePage;
|
||||||
|
|
||||||
Frame? innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
Frame? innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
||||||
if (innerShellFrame == null && frame == NavigationReferenceFrame.ShellFrame)
|
if (innerShellFrame == null && frame == NavigationReferenceFrame.ShellFrame)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using Wino.Calendar.ViewModels;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Views.Abstract;
|
||||||
|
|
||||||
|
public abstract class CalendarEventComposePageAbstract : BasePage<CalendarEventComposePageViewModel> { }
|
||||||
@@ -149,14 +149,26 @@
|
|||||||
<SplitView.Pane>
|
<SplitView.Pane>
|
||||||
<Grid Padding="0,0,0,6">
|
<Grid Padding="0,0,0,6">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Margin="14,12,14,0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Command="{x:Bind ViewModel.NewEventCommand}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}">
|
||||||
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="8">
|
||||||
|
<coreControls:WinoFontIcon FontSize="16" Icon="NewMail" />
|
||||||
|
<TextBlock Text="{x:Bind domain:Translator.CalendarEventCompose_NewEventButton}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<calendarControls:WinoCalendarView
|
<calendarControls:WinoCalendarView
|
||||||
x:Name="CalendarView"
|
x:Name="CalendarView"
|
||||||
Grid.Row="0"
|
Grid.Row="1"
|
||||||
Margin="0,12,0,0"
|
Margin="0,12,0,0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
DateClickedCommand="{x:Bind ViewModel.DateClickedCommand}"
|
DateClickedCommand="{x:Bind ViewModel.DateClickedCommand}"
|
||||||
@@ -167,7 +179,7 @@
|
|||||||
<!-- Account Calendars Host -->
|
<!-- Account Calendars Host -->
|
||||||
<ListView
|
<ListView
|
||||||
x:Name="CalendarHostListView"
|
x:Name="CalendarHostListView"
|
||||||
Grid.Row="1"
|
Grid.Row="2"
|
||||||
ItemsSource="{x:Bind ViewModel.AccountCalendarStateService.GroupedAccountCalendars}"
|
ItemsSource="{x:Bind ViewModel.AccountCalendarStateService.GroupedAccountCalendars}"
|
||||||
SelectionMode="None">
|
SelectionMode="None">
|
||||||
<ListView.Header>
|
<ListView.Header>
|
||||||
@@ -261,7 +273,7 @@
|
|||||||
|
|
||||||
<!-- Menu Items -->
|
<!-- Menu Items -->
|
||||||
<ListView
|
<ListView
|
||||||
Grid.Row="2"
|
Grid.Row="3"
|
||||||
ItemTemplateSelector="{StaticResource NavigationMenuTemplateSelector}"
|
ItemTemplateSelector="{StaticResource NavigationMenuTemplateSelector}"
|
||||||
SelectedIndex="{x:Bind ViewModel.SelectedMenuItemIndex, Mode=TwoWay}">
|
SelectedIndex="{x:Bind ViewModel.SelectedMenuItemIndex, Mode=TwoWay}">
|
||||||
<ListView.Items>
|
<ListView.Items>
|
||||||
|
|||||||
@@ -0,0 +1,533 @@
|
|||||||
|
<?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: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="12,8" />
|
||||||
|
<Setter Property="MinWidth" Value="0" />
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style
|
||||||
|
x:Key="ComposeCreateButtonStyle"
|
||||||
|
BasedOn="{StaticResource DefaultButtonStyle}"
|
||||||
|
TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
|
||||||
|
<Setter Property="Padding" Value="16,8" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="EventDetailsPanelGridStyle" TargetType="Grid">
|
||||||
|
<Setter Property="Padding" Value="12,6" />
|
||||||
|
<Setter Property="Margin" Value="0,12" />
|
||||||
|
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
|
||||||
|
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style
|
||||||
|
x:Key="EventDetailsPanelTitleStyle"
|
||||||
|
BasedOn="{StaticResource SubtitleTextBlockStyle}"
|
||||||
|
TargetType="TextBlock">
|
||||||
|
<Setter Property="FontWeight" Value="Normal" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,20" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="AttendeeSuggestionTemplate">
|
||||||
|
<StackPanel Padding="8,4" Orientation="Vertical">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||||
|
<TextBlock FontSize="12" Text="{Binding Address}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="AttendeeTokenTemplate">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</Page.Resources>
|
||||||
|
|
||||||
|
<Grid Padding="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="{ThemeResource WinoContentZoneBackgroud}"
|
||||||
|
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="7">
|
||||||
|
<ScrollViewer
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollMode="Enabled"
|
||||||
|
VerticalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollMode="Disabled">
|
||||||
|
<StackPanel
|
||||||
|
Padding="8"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<Button Command="{x:Bind ViewModel.CreateCommand}" Style="{StaticResource ComposeCreateButtonStyle}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<coreControls:WinoFontIcon FontSize="16" Icon="Save" />
|
||||||
|
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Create}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<coreControls:WinoFontIcon FontSize="16" Icon="CalendarShowAs" />
|
||||||
|
<ComboBox
|
||||||
|
Width="190"
|
||||||
|
DisplayMemberPath="Name"
|
||||||
|
ItemsSource="{x:Bind ViewModel.AvailableCalendars}"
|
||||||
|
PlaceholderText="{x:Bind domain:Translator.CalendarEventCompose_SelectCalendar}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedCalendar, Mode=TwoWay}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="2">
|
||||||
|
<TextBlock Text="{Binding Name}" />
|
||||||
|
<TextBlock
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{Binding Account.Address}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<coreControls:WinoFontIcon FontSize="16" Icon="CalendarShowAs" />
|
||||||
|
<ComboBox
|
||||||
|
Width="150"
|
||||||
|
DisplayMemberPath="DisplayText"
|
||||||
|
ItemsSource="{x:Bind ViewModel.ShowAsOptions}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedShowAsOption, Mode=TwoWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
||||||
|
<ComboBox
|
||||||
|
Width="150"
|
||||||
|
DisplayMemberPath="DisplayText"
|
||||||
|
ItemsSource="{x:Bind ViewModel.ReminderOptions}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedReminderOption, Mode=TwoWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<Grid ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid x:Name="DetailsGrid" Style="{StaticResource EventDetailsPanelGridStyle}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_Details}" />
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
Grid.Row="1"
|
||||||
|
Header="{x:Bind domain:Translator.CalendarEventCompose_Title}"
|
||||||
|
PlaceholderText="{x:Bind domain:Translator.CalendarEventCompose_TitlePlaceholder}"
|
||||||
|
Text="{x:Bind ViewModel.Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
Grid.Column="0"
|
||||||
|
Date="{x:Bind ViewModel.StartDate, Mode=TwoWay}"
|
||||||
|
Header="{x:Bind domain:Translator.CalendarEventCompose_StartDate}" />
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,30,0,0"
|
||||||
|
IsChecked="{x:Bind ViewModel.IsAllDay, Mode=TwoWay}">
|
||||||
|
<TextBlock Text="{x:Bind domain:Translator.CalendarEventCompose_AllDay}" />
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
|
<TimePicker
|
||||||
|
Grid.Column="2"
|
||||||
|
ClockIdentifier="{x:Bind ViewModel.TimePickerClockIdentifier, Mode=OneWay}"
|
||||||
|
Header="{x:Bind domain:Translator.CalendarEventCompose_StartTime}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.IsAllDay, Mode=OneWay}"
|
||||||
|
SelectedTime="{x:Bind ViewModel.StartTime, Mode=TwoWay}" />
|
||||||
|
|
||||||
|
<TimePicker
|
||||||
|
Grid.Column="3"
|
||||||
|
ClockIdentifier="{x:Bind ViewModel.TimePickerClockIdentifier, Mode=OneWay}"
|
||||||
|
Header="{x:Bind domain:Translator.CalendarEventCompose_EndTime}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.IsAllDay, Mode=OneWay}"
|
||||||
|
SelectedTime="{x:Bind ViewModel.EndTime, Mode=TwoWay}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<DatePicker
|
||||||
|
Grid.Row="3"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Date="{x:Bind ViewModel.AllDayEndDate, Mode=TwoWay}"
|
||||||
|
Header="{x:Bind domain:Translator.CalendarEventCompose_EndDate}"
|
||||||
|
Visibility="{x:Bind ViewModel.IsAllDay, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Grid.Row="4"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="0"
|
||||||
|
Header="{x:Bind domain:Translator.CalendarEventCompose_Location}"
|
||||||
|
PlaceholderText="{x:Bind domain:Translator.CalendarEventCompose_LocationPlaceholder}"
|
||||||
|
Text="{x:Bind ViewModel.Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,30,0,0"
|
||||||
|
IsChecked="{x:Bind ViewModel.IsRecurring, Mode=TwoWay}">
|
||||||
|
<TextBlock Text="{x:Bind domain:Translator.CalendarEventCompose_Recurring}" />
|
||||||
|
</ToggleButton>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Grid.Row="5"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
CornerRadius="6"
|
||||||
|
Visibility="{x:Bind ViewModel.IsRecurring, Mode=OneWay}">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<Grid ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="80" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="140" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="170" />
|
||||||
|
<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"
|
||||||
|
DisplayMemberPath="DisplayText"
|
||||||
|
ItemsSource="{x:Bind ViewModel.RecurrenceFrequencyOptions}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedRecurrenceFrequencyOption, Mode=TwoWay}" />
|
||||||
|
<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>
|
||||||
|
<ToggleButton
|
||||||
|
Width="32"
|
||||||
|
Height="32"
|
||||||
|
Padding="0"
|
||||||
|
IsChecked="{Binding IsSelected, Mode=TwoWay}">
|
||||||
|
<TextBlock HorizontalAlignment="Center" Text="{Binding 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>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="6"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{x:Bind ViewModel.RecurrenceSummary, Mode=OneWay}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="7" Margin="0,12,0,0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,0,0,8"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{x:Bind domain:Translator.CalendarEventCompose_Notes}" />
|
||||||
|
|
||||||
|
<mailControls:WebViewEditorControl
|
||||||
|
x:Name="NotesEditor"
|
||||||
|
Grid.Row="1"
|
||||||
|
Height="340"
|
||||||
|
IsEditorDarkMode="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=OneWay}"
|
||||||
|
IsEditorWebViewEditor="True" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
x:Name="PeopleGrid"
|
||||||
|
Grid.Column="1"
|
||||||
|
Style="{StaticResource EventDetailsPanelGridStyle}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_People}" />
|
||||||
|
|
||||||
|
<toolkit:TokenizingTextBox
|
||||||
|
x:Name="AttendeeBox"
|
||||||
|
Grid.Row="1"
|
||||||
|
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
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="-12,12,0,0"
|
||||||
|
ItemsSource="{x:Bind ViewModel.Attendees, Mode=OneWay}"
|
||||||
|
SelectionMode="None">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Margin="0,6" ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<controls:ImagePreviewControl
|
||||||
|
Width="40"
|
||||||
|
Height="40"
|
||||||
|
Address="{Binding Email}"
|
||||||
|
DisplayNameOverride="{Binding DisplayName}"
|
||||||
|
PreviewContact="{Binding ResolvedContact}" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Spacing="4">
|
||||||
|
<TextBlock
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{Binding DisplayName}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{Binding Email}"
|
||||||
|
Visibility="{Binding HasDistinctDisplayName}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
Click="RemoveAttendeeClicked"
|
||||||
|
Style="{StaticResource TransparentActionButtonStyle}"
|
||||||
|
Tag="{Binding}">
|
||||||
|
<coreControls:WinoFontIcon FontSize="14" Icon="Delete" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
x:Name="AttachmentsGrid"
|
||||||
|
Grid.Column="2"
|
||||||
|
Style="{StaticResource EventDetailsPanelGridStyle}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource EventDetailsPanelTitleStyle}" Text="{x:Bind domain:Translator.CalendarEventDetails_Attachments}" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Command="{x:Bind ViewModel.AddAttachmentsCommand}"
|
||||||
|
Style="{StaticResource TransparentActionButtonStyle}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<coreControls:WinoFontIcon FontSize="14" Icon="EventAccept" />
|
||||||
|
<TextBlock Text="{x:Bind domain:Translator.CalendarEventCompose_AddAttachment}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="-12,12,0,0"
|
||||||
|
ItemsSource="{x:Bind ViewModel.Attachments, Mode=OneWay}"
|
||||||
|
SelectionMode="None">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Height="56" ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="40" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ContentControl
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{Binding AttachmentType}"
|
||||||
|
ContentTemplateSelector="{StaticResource FileTypeIconSelector}" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock Text="{Binding FileName}" TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Text="{Binding ReadableSize}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Grid.Column="2"
|
||||||
|
Click="RemoveAttachmentClicked"
|
||||||
|
Style="{StaticResource TransparentActionButtonStyle}"
|
||||||
|
Tag="{Binding}">
|
||||||
|
<coreControls:WinoFontIcon FontSize="14" Icon="Delete" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="WindowWidthStates">
|
||||||
|
<VisualState x:Name="WideState">
|
||||||
|
<VisualState.StateTriggers>
|
||||||
|
<AdaptiveTrigger MinWindowWidth="1200" />
|
||||||
|
</VisualState.StateTriggers>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="MediumState">
|
||||||
|
<VisualState.StateTriggers>
|
||||||
|
<AdaptiveTrigger MinWindowWidth="800" />
|
||||||
|
</VisualState.StateTriggers>
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="DetailsGrid.(Grid.Row)" Value="0" />
|
||||||
|
<Setter Target="DetailsGrid.(Grid.Column)" Value="0" />
|
||||||
|
<Setter Target="DetailsGrid.(Grid.RowSpan)" Value="2" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.Row)" Value="0" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.Column)" Value="1" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.ColumnSpan)" Value="2" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.RowSpan)" Value="1" />
|
||||||
|
<Setter Target="AttachmentsGrid.(Grid.Row)" Value="1" />
|
||||||
|
<Setter Target="AttachmentsGrid.(Grid.Column)" Value="1" />
|
||||||
|
<Setter Target="AttachmentsGrid.(Grid.ColumnSpan)" Value="2" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="NarrowState">
|
||||||
|
<VisualState.StateTriggers>
|
||||||
|
<AdaptiveTrigger MinWindowWidth="0" />
|
||||||
|
</VisualState.StateTriggers>
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="DetailsGrid.(Grid.Row)" Value="0" />
|
||||||
|
<Setter Target="DetailsGrid.(Grid.Column)" Value="0" />
|
||||||
|
<Setter Target="DetailsGrid.(Grid.ColumnSpan)" Value="3" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.Row)" Value="1" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.Column)" Value="0" />
|
||||||
|
<Setter Target="PeopleGrid.(Grid.ColumnSpan)" Value="3" />
|
||||||
|
<Setter Target="AttachmentsGrid.(Grid.Row)" Value="2" />
|
||||||
|
<Setter Target="AttachmentsGrid.(Grid.Column)" Value="0" />
|
||||||
|
<Setter Target="AttachmentsGrid.(Grid.ColumnSpan)" Value="3" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Grid>
|
||||||
|
</abstract:CalendarEventComposePageAbstract>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user