1 Commits

Author SHA1 Message Date
google-labs-jules[bot]
a1463dfeb9 Output:
Fix: Correct print scaling for Win2D PDF printing

Issue:
When you're printing pages using Win2D, especially when dealing with PDF documents that are rendered to CanvasBitmaps, you might run into some problems if your Windows display scaling isn't set to 100%. You might find that the edges of your printed PDF page are cut off, or the content might look like it's spilling out of the paper boundaries.

Cause:
The problem was happening because a part of the code, `PdfDocument.GetPage().RenderToStreamAsync()`, was being used without specifying certain rendering options (`PdfPageRenderOptions`). This meant the PDF page was being turned into an image stream using a default resolution (probably assuming 96 DPI for the PDF's original size) and wasn't taking into account the printer's actual DPI or your system's display scaling factor (`RawPixelsPerViewPixel`). Because of this, the `CanvasBitmap` created from this stream had pixel dimensions that didn't accurately match the intended physical size on paper when display scaling was active. This led to an incorrect layout by `CanvasPrintDocument`.

Solution:
I've made changes to the `LoadPDFPageBitmapsAsync` method in `Wino.Core.UWP/Services/PrintService.cs` to address this. Here's what I did:
1. I now get the printer's DPI from the `CanvasPrintDocument` (`sender.Dpi`).
2. I also get your current system display scaling factor (`DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel`).
3. For each page in your PDF, I calculate the target pixel dimensions needed to print the page at its correct physical size, using the printer's DPI.
4. Then, I figure out the necessary `DestinationWidth` and `DestinationHeight` (in DIPs) for `PdfPageRenderOptions` by dividing those target pixel dimensions by the `rawPixelsPerViewPixel`.
5. Finally, I call `PdfPage.RenderToStreamAsync()` using these calculated options.

This makes sure that the `CanvasBitmap` objects are created with pixel dimensions that are right for the printer's resolution and are correctly scaled according to your system's display settings. I ran some checks by simulating how this would work with display scaling at 100%, 125%, and 150%, and it confirmed that the calculations are now correct.
2025-05-20 18:48:21 +00:00
95 changed files with 2171 additions and 3921 deletions

View File

@@ -19,7 +19,6 @@
<PackageVersion Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Primitives" Version="8.2.250129-preview2" />
<PackageVersion Include="EmailValidation" Version="1.3.0" />
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.0" />
<PackageVersion Include="Ical.Net" Version="4.3.1" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />

View File

@@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
@@ -142,16 +141,6 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
Type = CalendarSynchronizationType.CalendarMetadata
};
var timer = new Stopwatch();
var synchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(new NewCalendarSynchronizationRequested(synchronizationOptions, SynchronizationSource.Client));
timer.Stop();
Debug.WriteLine("Synchronization completed in {timer.ElapsedMilliseconds} ms");
// TODO: Properly handle synchronization errors.
accountCreationDialog.Complete(false);
}
}

View File

@@ -127,7 +127,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
private readonly IKeyPressService _keyPressService;
private readonly ICalendarServiceEx _calendarServiceEx;
private readonly IPreferencesService _preferencesService;
// Store latest rendered options.
@@ -147,7 +146,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
ICalendarService calendarService,
INavigationService navigationService,
IKeyPressService keyPressService,
ICalendarServiceEx calendarServiceEx,
IAccountCalendarStateService accountCalendarStateService,
IPreferencesService preferencesService)
{
@@ -157,7 +155,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
_calendarService = calendarService;
_navigationService = navigationService;
_keyPressService = keyPressService;
_calendarServiceEx = calendarServiceEx;
_preferencesService = preferencesService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
@@ -238,6 +235,23 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
[RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(CanSaveQuickEvent))]
private async Task SaveQuickEventAsync()
{
var durationSeconds = (QuickEventEndTime - QuickEventStartTime).TotalSeconds;
var testCalendarItem = new CalendarItem
{
CalendarId = SelectedQuickEventAccountCalendar.Id,
StartDate = QuickEventStartTime,
DurationInSeconds = durationSeconds,
CreatedAt = DateTime.UtcNow,
Description = string.Empty,
Location = Location,
Title = EventName,
Id = Guid.NewGuid()
};
IsQuickEventDialogOpen = false;
await _calendarService.CreateNewCalendarItemAsync(testCalendarItem, null);
// TODO: Create the request with the synchronizer.
}
@@ -621,13 +635,10 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Check all the events for the given date range and calendar.
// Then find the day representation for all the events returned, and add to the collection.
var events = await _calendarServiceEx.GetExpandedEventsInDateRangeWithExceptionsAsync(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End, calendarViewModel.AccountCalendar).ConfigureAwait(false);
var events = await _calendarService.GetCalendarEventsAsync(calendarViewModel, dayRangeRenderModel).ConfigureAwait(false);
foreach (var @event in events)
{
// TODO: Do it in the service.
@event.AssignedCalendar = calendarViewModel.AccountCalendar;
// Find the days that the event falls into.
var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period));
@@ -789,18 +800,19 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
// Recurring events must be selected as a single instance.
// We need to find the day that the event is in, and then select the event.
if (calendarItemViewModel == null) return Enumerable.Empty<CalendarItemViewModel>();
// If the calendar item is not recurring, we can just return it.
if (calendarItemViewModel.ItemType == CalendarItemType.Timed || calendarItemViewModel.ItemType == CalendarItemType.RecurringException) return new[] { calendarItemViewModel };
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
.Where(c => c != null)
.Cast<CalendarItemViewModel>()
.Distinct();
if (!calendarItemViewModel.IsRecurringEvent)
{
return [calendarItemViewModel];
}
else
{
return DayRanges
.SelectMany(a => a.CalendarDays)
.Select(b => b.EventsCollection.GetCalendarItem(calendarItemViewModel.Id))
.Where(c => c != null)
.Cast<CalendarItemViewModel>()
.Distinct();
}
}
private void UnselectCalendarItem(CalendarItemViewModel calendarItemViewModel, CalendarDayModel calendarDay = null)
@@ -809,8 +821,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
var itemsToUnselect = GetCalendarItems(calendarItemViewModel, calendarDay);
if (itemsToUnselect == null) return;
foreach (var item in itemsToUnselect)
{
item.IsSelected = false;
@@ -823,8 +833,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel,
var itemsToSelect = GetCalendarItems(calendarItemViewModel, calendarDay);
if (itemsToSelect == null) return;
foreach (var item in itemsToSelect)
{
item.IsSelected = true;

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data;
@@ -19,73 +17,28 @@ public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, IC
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
public DateTime StartDateTime { get => CalendarItem.StartDateTime; set => CalendarItem.StartDateTime = value; }
public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
public DateTime EndDateTime => CalendarItem.EndDateTime;
public DateTime EndDate => CalendarItem.EndDate;
/// <summary>
/// Gets the start date and time in the local time zone for display purposes.
/// </summary>
public DateTime LocalStartDateTime => ConvertToLocalTime();
/// <summary>
/// Gets the end date and time in the local time zone for display purposes.
/// </summary>
public DateTime LocalEndDateTime => ConvertToLocalTime();
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
public ITimePeriod Period => CalendarItem.Period;
public bool IsRecurringEvent => !string.IsNullOrEmpty(CalendarItem.RecurrenceRules) || !string.IsNullOrEmpty(CalendarItem.RecurringEventId);
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
[ObservableProperty]
private bool _isSelected;
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemType ItemType => CalendarItem.ItemType;
public CalendarItemViewModel(CalendarItem calendarItem)
{
CalendarItem = calendarItem;
Debug.WriteLine($"{Title} : {ItemType}");
}
/// <summary>
/// Converts a DateTime to local time based on the provided timezone.
/// If timezone is empty or null, assumes the DateTime is in UTC.
/// </summary>
/// <param name="dateTime">The DateTime to convert</param>
/// <param name="timeZone">The timezone string. If empty/null, assumes UTC.</param>
/// <returns>DateTime converted to local time</returns>
private DateTime ConvertToLocalTime()
{
// All day events ignore time zones and are treated as local time.
if (ItemType == CalendarItemType.AllDay || ItemType == CalendarItemType.MultiDayAllDay || ItemType == CalendarItemType.RecurringAllDay)
return CalendarItem.StartDateTime;
if (string.IsNullOrEmpty(CalendarItem.TimeZone))
{
// If no timezone specified, assume it's UTC and convert to local time
return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime();
}
try
{
// Parse the timezone and convert to local time
var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(CalendarItem.TimeZone);
return TimeZoneInfo.ConvertTimeToUtc(CalendarItem.StartDateTime, sourceTimeZone).ToLocalTime();
}
catch (TimeZoneNotFoundException)
{
// If timezone is not found, fallback to treating as UTC
return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime();
}
catch (InvalidTimeZoneException)
{
// If timezone is invalid, fallback to treating as UTC
return DateTime.SpecifyKind(CalendarItem.StartDateTime, DateTimeKind.Utc).ToLocalTime();
}
}
public override string ToString() => CalendarItem.Title;

View File

@@ -31,7 +31,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
[ObservableProperty]
private CalendarItemViewModel _seriesParent;
public bool CanViewSeries => false; //CurrentEvent?.IsRecurringChild ?? false; // TODO: Implement this properly
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
#endregion
@@ -67,7 +67,7 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel
CurrentEvent = new CalendarItemViewModel(currentEventItem);
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.Id);
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId);
foreach (var item in attendees)
{

308
Wino.Calendar.sln Normal file
View File

@@ -0,0 +1,308 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35424.110
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.Domain", "Wino.Core.Domain\Wino.Core.Domain.csproj", "{814400B6-5A05-4596-B451-3A116A147DC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.UWP", "Wino.Core.UWP\Wino.Core.UWP.csproj", "{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.ViewModels", "Wino.Core.ViewModels\Wino.Core.ViewModels.csproj", "{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Messaging", "Wino.Messages\Wino.Messaging.csproj", "{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Server", "Wino.Server\Wino.Server.csproj", "{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core", "Wino.Core\Wino.Core.csproj", "{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar", "Wino.Calendar\Wino.Calendar.csproj", "{600F4979-DB7E-409D-B7DA-B60BE4C55C35}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.SourceGenerators", "Wino.SourceGenerators\Wino.SourceGenerators.csproj", "{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}"
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Wino.Calendar.Packaging", "Wino.Calendar.Packaging\Wino.Calendar.Packaging.wapproj", "{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{CF850F8C-5042-4376-9CBA-C8F2BB554083}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Services", "Wino.Services\Wino.Services.csproj", "{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Authentication", "Wino.Authentication\Wino.Authentication.csproj", "{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM = Debug|ARM
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM = Release|ARM
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM64.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x64.ActiveCfg = Debug|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x64.Build.0 = Debug|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x86.ActiveCfg = Debug|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x86.Build.0 = Debug|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|Any CPU.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM64.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM64.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x64.ActiveCfg = Release|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x64.Build.0 = Release|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x86.ActiveCfg = Release|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x86.Build.0 = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM.ActiveCfg = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM.Build.0 = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.Build.0 = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.ActiveCfg = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.Build.0 = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.ActiveCfg = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.Build.0 = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|Any CPU.Build.0 = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM.ActiveCfg = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM.Build.0 = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.ActiveCfg = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.Build.0 = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.ActiveCfg = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.Build.0 = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.ActiveCfg = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.Build.0 = Release|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM64.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x64.ActiveCfg = Debug|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x64.Build.0 = Debug|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x86.ActiveCfg = Debug|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x86.Build.0 = Debug|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|Any CPU.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM64.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM64.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x64.ActiveCfg = Release|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x64.Build.0 = Release|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x86.ActiveCfg = Release|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x86.Build.0 = Release|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM64.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x64.ActiveCfg = Debug|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x64.Build.0 = Debug|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x86.ActiveCfg = Debug|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x86.Build.0 = Debug|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|Any CPU.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM64.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM64.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x64.ActiveCfg = Release|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x64.Build.0 = Release|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x86.ActiveCfg = Release|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x86.Build.0 = Release|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|Any CPU.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|Any CPU.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM64.Build.0 = Debug|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x64.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x64.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x86.ActiveCfg = Debug|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x86.Build.0 = Debug|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|Any CPU.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|Any CPU.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM64.ActiveCfg = Release|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM64.Build.0 = Release|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x64.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x64.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x86.ActiveCfg = Release|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x86.Build.0 = Release|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM64.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x64.ActiveCfg = Debug|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x64.Build.0 = Debug|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x86.ActiveCfg = Debug|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x86.Build.0 = Debug|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|Any CPU.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM64.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM64.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x64.ActiveCfg = Release|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x64.Build.0 = Release|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x86.ActiveCfg = Release|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.ActiveCfg = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.Build.0 = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.Deploy.0 = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.ActiveCfg = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Build.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Deploy.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.ActiveCfg = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Build.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Deploy.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.ActiveCfg = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.Build.0 = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.Deploy.0 = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.ActiveCfg = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Build.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Deploy.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.ActiveCfg = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Deploy.0 = Release|x86
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM64.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x64.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x86.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM64.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM64.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x64.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x64.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x86.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x86.Build.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.ActiveCfg = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.Build.0 = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.Deploy.0 = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Build.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Deploy.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.ActiveCfg = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Build.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Deploy.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.ActiveCfg = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Build.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Deploy.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.Build.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.Deploy.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.ActiveCfg = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.Build.0 = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.Deploy.0 = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.ActiveCfg = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Build.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Deploy.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.ActiveCfg = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Build.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Deploy.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.ActiveCfg = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Build.0 = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Deploy.0 = Release|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM64.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x64.ActiveCfg = Debug|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x64.Build.0 = Debug|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x86.ActiveCfg = Debug|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x86.Build.0 = Debug|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|Any CPU.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM64.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM64.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x64.ActiveCfg = Release|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x64.Build.0 = Release|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x86.ActiveCfg = Release|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x86.Build.0 = Release|x86
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM64.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x64.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x64.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x86.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x86.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|Any CPU.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM64.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM64.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x64.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x64.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x86.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x86.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM64.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x64.ActiveCfg = Debug|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x64.Build.0 = Debug|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x86.ActiveCfg = Debug|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x86.Build.0 = Debug|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|Any CPU.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM64.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM64.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x64.ActiveCfg = Release|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x64.Build.0 = Release|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x86.ActiveCfg = Release|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -69,7 +69,7 @@
VerticalAlignment="Top"
Orientation="Horizontal"
Spacing="6">
<!--<controls:WinoFontIcon
<controls:WinoFontIcon
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventRepeat"
@@ -79,7 +79,7 @@
FontSize="12"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(CalendarItem.AssignedCalendar.BackgroundColorHex), Mode=OneWay}"
Icon="CalendarEventMuiltiDay"
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />-->
Visibility="{x:Bind CalendarItem.IsMultiDayEvent, Mode=OneWay}" />
</StackPanel>
<VisualStateManager.VisualStateGroups>

View File

@@ -7,7 +7,6 @@ using Windows.UI.Xaml.Input;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
@@ -91,9 +90,7 @@ public sealed partial class CalendarItemControl : UserControl
if (CalendarItem == null) return;
if (DisplayingDate == null) return;
bool isMultiDayEvent = CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDay || CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay;
if (isMultiDayEvent)
if (CalendarItem.IsMultiDayEvent)
{
// Multi day events are divided into 3 categories:
// 1. All day events
@@ -106,14 +103,14 @@ public sealed partial class CalendarItemControl : UserControl
periodRelation == PeriodRelation.EnclosingStartTouching)
{
// hour -> title
CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDateTime.TimeOfDay)} -> {CalendarItem.Title}";
CalendarItemTitle = $"{DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.StartDate.TimeOfDay)} -> {CalendarItem.Title}";
}
else if (
periodRelation == PeriodRelation.EndInside ||
periodRelation == PeriodRelation.EnclosingEndTouching)
{
// title <- hour
CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDateTime.TimeOfDay)}";
CalendarItemTitle = $"{CalendarItem.Title} <- {DisplayingDate.CalendarRenderOptions.CalendarSettings.GetTimeString(CalendarItem.EndDate.TimeOfDay)}";
}
else if (periodRelation == PeriodRelation.Enclosing)
{
@@ -142,11 +139,11 @@ public sealed partial class CalendarItemControl : UserControl
{
if (CalendarItem == null) return;
if (CalendarItem.CalendarItem.ItemType == CalendarItemType.AllDay)
if (CalendarItem.IsAllDayEvent)
{
VisualStateManager.GoToState(this, "AllDayEvent", true);
}
else if (CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay || CalendarItem.CalendarItem.ItemType == CalendarItemType.MultiDay)
else if (CalendarItem.IsMultiDayEvent)
{
if (IsCustomEventArea)
{

View File

@@ -9,7 +9,6 @@ using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Calendar.Models;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.Controls;
@@ -45,9 +44,9 @@ public partial class WinoCalendarPanel : Panel
private void ResetMeasurements() => _measurements.Clear();
private double GetChildTopMargin(ICalendarItemViewModel calendarItemViewModel, double availableHeight)
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
{
var childStart = calendarItemViewModel.LocalStartDateTime;
var childStart = calendarItemViewModel.StartDate;
if (childStart <= Period.Start)
{
@@ -69,10 +68,10 @@ public partial class WinoCalendarPanel : Panel
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
=> availableWidth * calendarItemMeasurement.Left;
private double GetChildHeight(ICalendarItemViewModel child)
private double GetChildHeight(ICalendarItem child)
{
// All day events are not measured.
if (child.ItemType == CalendarItemType.AllDay) return 0;
if (child.IsAllDayEvent) return 0;
double childDurationInMinutes = 0d;
double availableHeight = HourHeight * 24;
@@ -81,7 +80,7 @@ public partial class WinoCalendarPanel : Panel
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
if (child.ItemType != CalendarItemType.MultiDay || child.ItemType != CalendarItemType.MultiDayAllDay)
if (!child.IsMultiDayEvent)
{
childDurationInMinutes = child.Period.Duration.TotalMinutes;
}
@@ -128,7 +127,7 @@ public partial class WinoCalendarPanel : Panel
foreach (var control in calendarControls)
{
// We can't arrange this child.
if (!(control.Content is ICalendarItemViewModel child)) continue;
if (!(control.Content is ICalendarItem child)) continue;
bool isHorizontallyLastItem = false;
@@ -161,7 +160,7 @@ public partial class WinoCalendarPanel : Panel
double extraRightMargin = 0;
// Multi-day events don't have any margin and their hit test is disabled.
if (child.ItemType != CalendarItemType.MultiDay || child.ItemType != CalendarItemType.MultiDayAllDay)
if (!child.IsMultiDayEvent)
{
// Max of 5% of the width or 20px max.
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
@@ -169,10 +168,8 @@ public partial class WinoCalendarPanel : Panel
if (childWidth < 0) childWidth = 1;
bool isAllOrMultiDayEvent = child.ItemType == CalendarItemType.AllDay || child.ItemType == CalendarItemType.MultiDay || child.ItemType == CalendarItemType.MultiDayAllDay;
// Regular events must have 2px margin
if (!isAllOrMultiDayEvent)
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
{
childLeft += 2;
childTop += 2;
@@ -214,12 +211,10 @@ public partial class WinoCalendarPanel : Panel
var columns = new List<List<ICalendarItem>>();
DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.StartDateTime).ThenBy(ev => ev.EndDateTime))
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
{
// Multi-day events are not measured.
bool isMultiDayEvent = ev.ItemType == CalendarItemType.MultiDay || ev.ItemType == CalendarItemType.MultiDayAllDay;
if (isMultiDayEvent) continue;
if (ev.IsMultiDayEvent) continue;
if (ev.Period.Start >= lastEventEnding)
{

View File

@@ -22,7 +22,6 @@ public static class CalendarXamlHelpers
/// </summary>
public static string GetEventDetailsDateString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
// TODO: This is not correct.
if (calendarItemViewModel == null || settings == null) return string.Empty;
var start = calendarItemViewModel.Period.Start;
@@ -31,7 +30,7 @@ public static class CalendarXamlHelpers
string timeFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "h:mm tt" : "HH:mm";
string dateFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "dddd, dd MMMM h:mm tt" : "dddd, dd MMMM HH:mm";
if (calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDay || calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay)
if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{start.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)} - {end.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)}";
}
@@ -43,14 +42,54 @@ public static class CalendarXamlHelpers
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
{
// TODO
return string.Empty;
if (calendarItemViewModel == null || !calendarItemViewModel.IsRecurringChild) return string.Empty;
// Parse recurrence rules
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(calendarItemViewModel.StartDate),
End = new CalDateTime(calendarItemViewModel.EndDate),
};
var recurrenceLines = Regex.Split(calendarItemViewModel.CalendarItem.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
if (calendarEvent.RecurrenceRules == null || !calendarEvent.RecurrenceRules.Any())
{
return "No recurrence pattern.";
}
var recurrenceRule = calendarEvent.RecurrenceRules.First();
var daysOfWeek = string.Join(", ", recurrenceRule.ByDay.Select(day => day.DayOfWeek.ToString()));
string timeZone = calendarEvent.DtStart.TzId ?? "UTC";
return $"Every {daysOfWeek}, effective {calendarEvent.DtStart.Value.ToShortDateString()} " +
$"from {calendarEvent.DtStart.Value.ToShortTimeString()} to {calendarEvent.DtEnd.Value.ToShortTimeString()} " +
$"{timeZone}.";
}
public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
// TODO
return string.Empty;
if (calendarItemViewModel == null || settings == null) return string.Empty;
// Single event in a day.
if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}";
}
else if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}";
}
else
{
// All day event.
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})";
}
}
public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
@@ -59,12 +98,8 @@ public static class CalendarXamlHelpers
{
if (calendarItemViewModel == null) return PopupPlacementMode.Auto;
bool isAllDayOrMultiDay = calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDay ||
calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.AllDay ||
calendarItemViewModel.CalendarItem.ItemType == CalendarItemType.MultiDayAllDay;
// All and/or multi day events always go to the top of the screen.
if (isAllDayOrMultiDay) return PopupPlacementMode.Bottom;
if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom;
return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType);
}

View File

@@ -13,8 +13,7 @@ public partial class CustomAreaCalendarItemSelector : DataTemplateSelector
{
if (item is CalendarItemViewModel calendarItemViewModel)
{
return calendarItemViewModel.CalendarItem.ItemType == Core.Domain.Enums.CalendarItemType.MultiDay ||
calendarItemViewModel.CalendarItem.ItemType == Core.Domain.Enums.CalendarItemType.MultiDayAllDay ? MultiDayTemplate : AllDayTemplate;
return calendarItemViewModel.IsMultiDayEvent ? MultiDayTemplate : AllDayTemplate;
}
return base.SelectTemplateCore(item, container);

View File

@@ -89,7 +89,6 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
BorderBrush="Transparent"
IsTabStop="False"
PlaceholderText="Search" />
<StackPanel

View File

@@ -256,7 +256,7 @@
<PersonPicture
Width="40"
Height="40"
DisplayName="{x:Bind DisplayName}" />
DisplayName="{x:Bind Name}" />
<!-- TODO: Organizer -->
<Grid Grid.Column="1">
@@ -265,7 +265,7 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontWeight="SemiBold" Text="{x:Bind DisplayName}" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
<TextBlock
Grid.Row="1"
FontSize="13"

View File

@@ -80,11 +80,11 @@ public class CalendarEventCollection
// Multi-day events go to both.
// Anything else goes to regular.
if (calendarItem.ItemType == Enums.CalendarItemType.AllDay || calendarItem.ItemType == Enums.CalendarItemType.MultiDayAllDay || calendarItem.ItemType == Enums.CalendarItemType.RecurringAllDay)
if (calendarItem.IsAllDayEvent)
{
return [_internalAllDayEvents];
}
else if (calendarItem.ItemType == Enums.CalendarItemType.MultiDay || calendarItem.ItemType == Enums.CalendarItemType.MultiDayAllDay)
else if (calendarItem.IsMultiDayEvent)
{
return [_internalRegularEvents, _internalAllDayEvents];
}

View File

@@ -8,6 +8,8 @@ public static class Constants
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
public const string LocalDraftStartPrefix = "localDraft_";
public const string CalendarEventRecurrenceRuleSeperator = "___";
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey);

View File

@@ -7,40 +7,12 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class AccountCalendar : IAccountCalendar
{
[PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
[NotNull]
public string RemoteCalendarId { get; set; } = string.Empty;
[NotNull]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
[NotNull]
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Location { get; set; }
public string? TimeZone { get; set; }
public string? AccessRole { get; set; }
public bool IsPrimary { get; set; } = false;
public string? BackgroundColor { get; set; }
public string? ForegroundColor { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
public DateTime? LastSyncTime { get; set; }
public string? SynchronizationDeltaToken { get; set; }
public bool IsDeleted { get; set; } = false;
public string RemoteCalendarId { get; set; }
public string SynchronizationDeltaToken { get; set; }
public string Name { get; set; }
public bool IsPrimary { get; set; }
public bool IsExtended { get; set; } = true;
/// <summary>
@@ -48,4 +20,5 @@ public class AccountCalendar : IAccountCalendar
/// </summary>
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; }
}

View File

@@ -8,29 +8,12 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class CalendarEventAttendee
{
[PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
[NotNull]
public Guid EventId { get; set; }
[NotNull]
public string Email { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public AttendeeResponseStatus ResponseStatus { get; set; } = AttendeeResponseStatus.NeedsAction;
public bool IsOptional { get; set; } = false;
public bool IsOrganizer { get; set; } = false;
public bool IsSelf { get; set; } = false;
public string? Comment { get; set; }
public int? AdditionalGuests { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public AttendeeStatus AttendenceStatus { get; set; }
public bool IsOrganizer { get; set; }
public bool IsOptionalAttendee { get; set; }
public string Comment { get; set; }
}

View File

@@ -3,7 +3,6 @@ using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Helpers;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
@@ -12,81 +11,169 @@ namespace Wino.Core.Domain.Entities.Calendar;
public class CalendarItem : ICalendarItem
{
[PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
public Guid Id { get; set; }
public string RemoteEventId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Location { get; set; }
[NotNull]
public string RemoteEventId { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
[NotNull]
public Guid CalendarId { get; set; }
public DateTime EndDate
{
get
{
return StartDate.AddSeconds(DurationInSeconds);
}
}
[Ignore]
public IAccountCalendar AssignedCalendar { get; set; }
[NotNull]
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Location { get; set; }
public string? HtmlLink { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
{
get
{
_period ??= new TimeRange(StartDateTime, EndDateTime);
_period ??= new TimeRange(StartDate, EndDate);
return _period;
}
}
public bool IsAllDay { get; set; }
public string? TimeZone { get; set; }
public string? RecurrenceRules { get; set; }
public string? Status { get; set; }
public string? OrganizerDisplayName { get; set; }
public string? OrganizerEmail { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime LastModified { get; set; }
public bool IsDeleted { get; set; }
public string? RecurringEventId { get; set; }
public string? OriginalStartTime { get; set; }
/// <summary>
/// The type of calendar item (Timed, AllDay, MultiDay, etc.)
/// Events that starts at midnight and ends at midnight are considered all-day events.
/// </summary>
public CalendarItemType ItemType { get; set; }
/// <summary>
/// Automatically determines and sets the ItemType based on event properties
/// </summary>
public void DetermineItemType()
public bool IsAllDayEvent
{
var hasRecurrence = !string.IsNullOrEmpty(RecurrenceRules);
var isCancelled = Status?.ToLowerInvariant() == "cancelled" || IsDeleted;
get
{
return
StartDate.TimeOfDay == TimeSpan.Zero &&
EndDate.TimeOfDay == TimeSpan.Zero;
}
}
ItemType = CalendarItemTypeHelper.DetermineItemType(
StartDateTime,
EndDateTime,
IsAllDay,
hasRecurrence,
isCancelled,
Status);
/// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences.
/// IsOccurrence is used to display occurrence instances of parent recurring events.
/// IsOccurrence == false && IsRecurringChild == true => exceptional single instance.
/// </summary>
public bool IsRecurringChild
{
get
{
return RecurringCalendarItemId != null;
}
}
/// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences.
/// </summary>
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
/// <summary>
/// Events that are the master event definition of recurrence events.
/// </summary>
public bool IsRecurringParent
{
get
{
return !string.IsNullOrEmpty(Recurrence) && RecurringCalendarItemId == null;
}
}
/// <summary>
/// Events that are not all-day events and last more than one day are considered multi-day events.
/// </summary>
public bool IsMultiDayEvent
{
get
{
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent;
}
}
public double DurationInSeconds { get; set; }
public string Recurrence { get; set; }
public string OrganizerDisplayName { get; set; }
public string OrganizerEmail { get; set; }
/// <summary>
/// The id of the parent calendar item of the recurring event.
/// Exceptional instances are stored as a separate calendar item.
/// This makes the calendar item a child of the recurring event.
/// </summary>
public Guid? RecurringCalendarItemId { get; set; }
/// <summary>
/// Indicates read-only events. Default is false.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// Hidden events must not be displayed to the user.
/// This usually happens when a child instance of recurring parent is cancelled after creation.
/// </summary>
public bool IsHidden { get; set; }
// TODO
public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; }
public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; }
[Ignore]
public IAccountCalendar AssignedCalendar { get; set; }
/// <summary>
/// Whether this item does not really exist in the database or not.
/// These are used to display occurrence instances of parent recurring events.
/// </summary>
[Ignore]
public bool IsOccurrence { get; set; }
/// <summary>
/// Id to load information related to this event.
/// Occurrences tracked by the parent recurring event if they are not exceptional instances.
/// Recurring children here are exceptional instances. They have their own info in the database including Id.
/// </summary>
public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id;
public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds)
{
// Create a copy with the new start date and duration
return new CalendarItem
{
Id = Guid.NewGuid(),
Title = Title,
Description = Description,
Location = Location,
StartDate = startDate,
DurationInSeconds = durationInSeconds,
Recurrence = Recurrence,
OrganizerDisplayName = OrganizerDisplayName,
OrganizerEmail = OrganizerEmail,
RecurringCalendarItemId = Id,
AssignedCalendar = AssignedCalendar,
CalendarId = CalendarId,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
Visibility = Visibility,
Status = Status,
CustomEventColorHex = CustomEventColorHex,
HtmlLink = HtmlLink,
StartDateOffset = StartDateOffset,
EndDateOffset = EndDateOffset,
RemoteEventId = RemoteEventId,
IsHidden = IsHidden,
IsLocked = IsLocked,
IsOccurrence = true
};
}
}

View File

@@ -1,14 +0,0 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
public class Thumbnail
{
[PrimaryKey]
public string Domain { get; set; }
public string Gravatar { get; set; }
public string Favicon { get; set; }
public DateTime LastUpdated { get; set; }
}

View File

@@ -1,32 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Represents the response status of an attendee to a calendar event
/// </summary>
public enum AttendeeResponseStatus
{
/// <summary>
/// The attendee has not responded to the invitation
/// </summary>
NeedsAction = 0,
/// <summary>
/// The attendee has accepted the invitation
/// </summary>
Accepted = 1,
/// <summary>
/// The attendee has declined the invitation
/// </summary>
Declined = 2,
/// <summary>
/// The attendee has tentatively accepted the invitation
/// </summary>
Tentative = 3
}

View File

@@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Enums;
public enum CalendarItemType
{
/// <summary>
/// A standard timed event with specific start and end times on the same day
/// </summary>
Timed = 0,
/// <summary>
/// An all-day event that spans exactly one day
/// </summary>
AllDay = 1,
/// <summary>
/// A multi-day event that spans more than one day but has specific times
/// </summary>
MultiDay = 2,
/// <summary>
/// A multi-day all-day event (e.g., vacation, conference spanning multiple days)
/// </summary>
MultiDayAllDay = 3,
/// <summary>
/// A recurring event with a defined pattern (daily, weekly, monthly, yearly)
/// </summary>
Recurring = 4,
/// <summary>
/// A recurring all-day event (e.g., annual holiday, weekly all-day event)
/// </summary>
RecurringAllDay = 5,
/// <summary>
/// A single instance of a recurring event that has been modified
/// </summary>
RecurringException = 6,
/// <summary>
/// An event that extends beyond midnight but is not multi-day (e.g., 11 PM to 2 AM)
/// </summary>
CrossMidnight = 7,
}

View File

@@ -1,143 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Helpers;
/// <summary>
/// Helper class for CalendarItemType operations
/// </summary>
public static class CalendarItemTypeHelper
{
/// <summary>
/// Determines the calendar item type based on event properties
/// </summary>
/// <param name="startDateTime">Event start date/time</param>
/// <param name="endDateTime">Event end date/time</param>
/// <param name="isAllDay">Whether the event is marked as all-day</param>
/// <param name="isRecurring">Whether the event has recurrence rules</param>
/// <param name="isCancelled">Whether the event is cancelled</param>
/// <param name="status">Event status</param>
/// <returns>The appropriate CalendarItemType</returns>
public static CalendarItemType DetermineItemType(
DateTime startDateTime,
DateTime endDateTime,
bool isAllDay,
bool isRecurring = false,
bool isCancelled = false,
string? status = null)
{
// Handle recurring events
if (isRecurring)
{
return isAllDay ? CalendarItemType.RecurringAllDay : CalendarItemType.Recurring;
}
// Handle all-day events
if (isAllDay)
{
var daySpan = (endDateTime.Date - startDateTime.Date).Days;
return daySpan > 1 ? CalendarItemType.MultiDayAllDay : CalendarItemType.AllDay;
}
// Handle timed events
var duration = endDateTime - startDateTime;
// Multi-day timed events
if (duration.TotalDays >= 1)
{
return CalendarItemType.MultiDay;
}
// Cross midnight events (same calendar day but extends past midnight)
if (startDateTime.Date != endDateTime.Date && duration.TotalHours <= 24)
{
return CalendarItemType.CrossMidnight;
}
// Standard timed events
return CalendarItemType.Timed;
}
/// <summary>
/// Gets a human-readable description of the calendar item type
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>Description string</returns>
public static string GetDescription(CalendarItemType itemType)
{
return itemType switch
{
CalendarItemType.Timed => "Timed Event",
CalendarItemType.AllDay => "All-Day Event",
CalendarItemType.MultiDay => "Multi-Day Event",
CalendarItemType.MultiDayAllDay => "Multi-Day All-Day Event",
CalendarItemType.Recurring => "Recurring Event",
CalendarItemType.RecurringAllDay => "Recurring All-Day Event",
CalendarItemType.RecurringException => "Modified Recurring Event",
CalendarItemType.CrossMidnight => "Cross-Midnight Event",
_ => "Unknown Event Type"
};
}
/// <summary>
/// Checks if the event type represents an all-day event
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>True if it's an all-day event type</returns>
public static bool IsAllDayType(CalendarItemType itemType)
{
return itemType == CalendarItemType.AllDay ||
itemType == CalendarItemType.MultiDayAllDay ||
itemType == CalendarItemType.RecurringAllDay;
}
/// <summary>
/// Checks if the event type represents a recurring event
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>True if it's a recurring event type</returns>
public static bool IsRecurringType(CalendarItemType itemType)
{
return itemType == CalendarItemType.Recurring ||
itemType == CalendarItemType.RecurringAllDay ||
itemType == CalendarItemType.RecurringException;
}
/// <summary>
/// Checks if the event type represents a multi-day event
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>True if it's a multi-day event type</returns>
public static bool IsMultiDayType(CalendarItemType itemType)
{
return itemType == CalendarItemType.MultiDay ||
itemType == CalendarItemType.MultiDayAllDay;
}
/// <summary>
/// Gets the priority level for sorting events (lower number = higher priority)
/// </summary>
/// <param name="itemType">The calendar item type</param>
/// <returns>Priority number for sorting</returns>
public static int GetSortPriority(CalendarItemType itemType)
{
return itemType switch
{
CalendarItemType.AllDay => 2,
CalendarItemType.MultiDayAllDay => 3,
CalendarItemType.Timed => 4,
CalendarItemType.CrossMidnight => 5,
CalendarItemType.MultiDay => 6,
CalendarItemType.Recurring => 7,
CalendarItemType.RecurringAllDay => 8,
CalendarItemType.RecurringException => 9,
_ => 99
};
}
}

View File

@@ -1,6 +1,5 @@
using System;
using Itenso.TimePeriod;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
@@ -9,8 +8,15 @@ public interface ICalendarItem
string Title { get; }
Guid Id { get; }
IAccountCalendar AssignedCalendar { get; }
DateTime StartDateTime { get; set; }
DateTime EndDateTime { get; }
DateTime StartDate { get; set; }
DateTime EndDate { get; }
double DurationInSeconds { get; set; }
ITimePeriod Period { get; }
CalendarItemType ItemType { get; }
bool IsAllDayEvent { get; }
bool IsMultiDayEvent { get; }
bool IsRecurringChild { get; }
bool IsRecurringParent { get; }
bool IsRecurringEvent { get; }
}

View File

@@ -1,13 +1,9 @@
using System;
namespace Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
/// </summary>
public interface ICalendarItemViewModel : ICalendarItem
public interface ICalendarItemViewModel
{
bool IsSelected { get; set; }
DateTime LocalStartDateTime { get; }
DateTime LocalEndDateTime { get; }
}

View File

@@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
public interface ICalendarServiceEx
{
Task<int> ClearAllCalendarEventAttendeesAsync();
Task<int> ClearAllCalendarsAsync();
Task<int> ClearAllDataAsync();
Task<int> ClearAllEventsAsync();
Task<int> DeleteCalendarAsync(string remoteCalendarId);
Task<int> DeleteCalendarEventAttendeesForEventAsync(Guid eventId);
Task<int> DeleteEventAsync(string remoteEventId);
Task<List<CalendarEventAttendee>> GetAllCalendarEventAttendeesAsync();
Task<List<AccountCalendar>> GetAllCalendarsAsync();
Task<List<CalendarItem>> GetAllDayEventsAsync();
Task<List<CalendarItem>> GetAllEventsAsync();
Task<List<CalendarItem>> GetAllEventsIncludingDeletedAsync();
Task<List<CalendarItem>> GetAllRecurringEventsByTypeAsync();
Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId);
Task<Dictionary<AttendeeResponseStatus, int>> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId);
Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventAsync(Guid eventId);
Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId);
Task<string> GetCalendarSyncTokenAsync(string calendarId);
Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId);
Task<List<CalendarItem>> GetEventsByItemTypeAsync(CalendarItemType itemType);
Task<List<CalendarItem>> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes);
Task<List<CalendarItem>> GetEventsByremoteCalendarIdAsync(string remoteCalendarId);
Task<List<CalendarItem>> GetEventsForCalendarAsync(Guid calendarId);
Task<List<CalendarItem>> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate);
Task<List<CalendarItem>> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime);
Task<Dictionary<CalendarItemType, int>> GetEventStatsByItemTypeAsync();
Task<List<CalendarItem>> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate);
Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar);
Task<DateTime?> GetLastSyncTimeAsync(string calendarId);
Task<List<CalendarItem>> GetMultiDayEventsAsync();
Task<List<CalendarItem>> GetRecurringEventsAsync();
Task<int> HardDeleteEventAsync(string remoteEventId);
Task<int> InsertCalendarAsync(AccountCalendar calendar);
Task<int> InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee);
Task<int> InsertEventAsync(CalendarItem calendarItem);
Task<int> MarkEventAsDeletedAsync(string remoteEventId, string remoteCalendarId);
Task<int> SyncAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> attendees);
Task<int> SyncCalendarEventAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> calendareventattendees);
Task<int> UpdateAllEventItemTypesAsync();
Task<int> UpdateCalendarAsync(AccountCalendar calendar);
Task<int> UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee);
Task<int> UpdateCalendarSyncTokenAsync(string calendarId, string syncToken);
Task<int> UpdateEventAsync(CalendarItem calendarItem);
Task<int> UpsertCalendarAsync(AccountCalendar calendar);
Task<int> UpsertEventAsync(CalendarItem calendarItem);
}

View File

@@ -1,12 +1,11 @@
using System;
using System.ComponentModel;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Reader;
namespace Wino.Core.Domain.Interfaces;
public interface IPreferencesService: INotifyPropertyChanged
public interface IPreferencesService
{
/// <summary>
/// When any of the preferences are changed.
@@ -189,21 +188,6 @@ public interface IPreferencesService: INotifyPropertyChanged
/// </summary>
bool IsMailListActionBarEnabled { get; set; }
/// <summary>
/// Setting: Whether the mail rendering page will show the action labels
/// </summary>
bool IsShowActionLabelsEnabled { get; set; }
/// <summary>
/// Setting: Enable/disable Gravatar for sender avatars.
/// </summary>
bool IsGravatarEnabled { get; set; }
/// <summary>
/// Setting: Enable/disable Favicon for sender avatars.
/// </summary>
bool IsFaviconEnabled { get; set; }
#endregion
#region Calendar

View File

@@ -1,20 +0,0 @@
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
public interface IThumbnailService
{
/// <summary>
/// Clears the thumbnail cache.
/// </summary>
Task ClearCache();
/// <summary>
/// Gets thumbnail
/// </summary>
/// <param name="email">Address for thumbnail</param>
/// <param name="awaitLoad">Force to wait for thumbnail loading.
/// Should be used in non-UI threads or where delay is acceptable
/// </param>
ValueTask<string> GetThumbnailAsync(string email, bool awaitLoad = false);
}

View File

@@ -519,7 +519,7 @@
"SettingsCustomTheme_Title": "Custom Theme",
"SettingsDeleteAccount_Description": "Delete all e-mails and credentials associated with this account.",
"SettingsDeleteAccount_Title": "Delete this account",
"SettingsDeleteProtection_Description": "Should Wino ask you for confirmation every time you try to permanently delete a mail using Shift + Del keys?",
"SettingsDeleteProtection_Description": "Should Wino ask you for comfirmation every time you try to permanently delete a mail using Shift + Del keys?",
"SettingsDeleteProtection_Title": "Permanent Delete Protection",
"SettingsDiagnostics_Description": "For developers",
"SettingsDiagnostics_DiagnosticId_Description": "Share this ID with the developers when asked to get help for the issues you experience in Wino Mail.",
@@ -623,11 +623,6 @@
"SettingsShowPreviewText_Title": "Show Preview Text",
"SettingsShowSenderPictures_Description": "Hide/show the thumbnail sender pictures.",
"SettingsShowSenderPictures_Title": "Show Sender Avatars",
"SettingsEnableGravatarAvatars_Title": "Gravatar",
"SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture",
"SettingsEnableFavicons_Title": "Domain icons (Favicons)",
"SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture",
"SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars",
"SettingsSignature_AddCustomSignature_Button": "Add signature",
"SettingsSignature_AddCustomSignature_Title": "Add custom signature",
"SettingsSignature_DeleteSignature_Title": "Delete signature",
@@ -649,8 +644,6 @@
"SettingsThreads_Title": "Conversation Threading",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
"SettingsMailRendering_ActionLabels_Description": "Show action labels.",
"SignatureDeleteDialog_Message": "Are you sure you want to delete \"{0}\" signature?",
"SignatureDeleteDialog_Title": "Delete signature",
"SignatureEditorDialog_SignatureName_Placeholder": "Name your signature",

View File

@@ -59,7 +59,7 @@
<Setter Property="ChildrenTransitions">
<Setter.Value>
<TransitionCollection>
<EntranceThemeTransition IsStaggeringEnabled="False" />
<EntranceThemeTransition IsStaggeringEnabled="True" />
</TransitionCollection>
</Setter.Value>
</Setter>
@@ -96,19 +96,6 @@
</Setter.Value>
</Setter>
</Style>
<!-- Spacing between cards -->
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style (inc. the correct spacing) of a section header -->
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Style.Setters>
<Setter Property="Margin" Value="1,30,0,6" />
</Style.Setters>
</Style>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -25,7 +25,7 @@ public static class CoreUWPContainerSetup
services.AddSingleton<IPreferencesService, PreferencesService>();
services.AddSingleton<IThemeService, ThemeService>();
services.AddSingleton<IStatePersistanceService, StatePersistenceService>();
services.AddSingleton<IThumbnailService, ThumbnailService>();
services.AddSingleton<IDialogServiceBase, DialogServiceBase>();
services.AddTransient<IConfigurationService, ConfigurationService>();
services.AddTransient<IFileService, FileService>();

View File

@@ -23,6 +23,6 @@
<StackPanel Spacing="12">
<TextBlock x:Name="DialogDescription" TextWrapping="Wrap" />
<TextBox x:Name="FolderTextBox" Text="{x:Bind CurrentInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBox x:Name="FolderTextBox" Text="{x:Bind CurrentInput, Mode=TwoWay}" />
</StackPanel>
</ContentDialog>

View File

@@ -45,6 +45,7 @@ public class NativeAppService : INativeAppService
return _mimeMessagesFolder;
}
public async Task<string> GetEditorBundlePathAsync()
{
if (string.IsNullOrEmpty(_editorBundlePath))

View File

@@ -11,7 +11,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using System.IO;
using Wino.Core.Services;
namespace Wino.Core.UWP.Services;
@@ -23,19 +23,16 @@ public class NotificationBuilder : INotificationBuilder
private readonly IAccountService _accountService;
private readonly IFolderService _folderService;
private readonly IMailService _mailService;
private readonly IThumbnailService _thumbnailService;
public NotificationBuilder(IUnderlyingThemeService underlyingThemeService,
IAccountService accountService,
IFolderService folderService,
IMailService mailService,
IThumbnailService thumbnailService)
IMailService mailService)
{
_underlyingThemeService = underlyingThemeService;
_accountService = accountService;
_folderService = folderService;
_mailService = mailService;
_thumbnailService = thumbnailService;
}
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> downloadedMailItems)
@@ -86,16 +83,24 @@ public class NotificationBuilder : INotificationBuilder
var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default);
var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
if (!string.IsNullOrEmpty(avatarThumbnail))
var host = ThumbnailService.GetHost(mailItem.FromAddress);
var knownTuple = ThumbnailService.CheckIsKnown(host);
bool isKnown = knownTuple.Item1;
host = knownTuple.Item2;
if (isKnown)
builder.AddAppLogoOverride(new System.Uri(ThumbnailService.GetKnownHostImage(host)), hintCrop: ToastGenericAppLogoCrop.Default);
else
{
var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting);
await using (var stream = await tempFile.OpenStreamForWriteAsync())
{
var bytes = Convert.FromBase64String(avatarThumbnail);
await stream.WriteAsync(bytes);
}
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default);
// TODO: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=toolkit
// Follow official guides for icons/theme.
bool isOSDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark();
string profileLogoName = isOSDarkTheme ? "profile-dark.png" : "profile-light.png";
builder.AddAppLogoOverride(new System.Uri($"ms-appx:///Assets/NotificationIcons/{profileLogoName}"), hintCrop: ToastGenericAppLogoCrop.Circle);
}
// Override system notification timetamp with received date of the mail.
@@ -109,9 +114,9 @@ public class NotificationBuilder : INotificationBuilder
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
builder.AddButton(GetMarkedAsRead(mailItem.UniqueId));
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
builder.AddButton(GetDismissButton());
builder.AddAudio(new ToastAudio()
{
Src = new Uri("ms-winsoundevent:Notification.Mail")
@@ -134,14 +139,6 @@ public class NotificationBuilder : INotificationBuilder
.SetDismissActivation()
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png"));
private static ToastButton GetArchiveButton(Guid mailUniqueId)
=> new ToastButton()
.SetContent(Translator.MailOperation_Archive)
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/archive.png"))
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
.AddArgument(Constants.ToastActionKey, MailOperation.Archive)
.SetBackgroundActivation();
private ToastButton GetDeleteButton(Guid mailUniqueId)
=> new ToastButton()
.SetContent(Translator.MailOperation_Delete)
@@ -150,7 +147,7 @@ public class NotificationBuilder : INotificationBuilder
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete)
.SetBackgroundActivation();
private static ToastButton GetMarkAsReadButton(Guid mailUniqueId)
private ToastButton GetMarkedAsRead(Guid mailUniqueId)
=> new ToastButton()
.SetContent(Translator.MailOperation_MarkAsRead)
.SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png"))

View File

@@ -13,12 +13,17 @@ using Wino.Services;
namespace Wino.Core.UWP.Services;
public class PreferencesService(IConfigurationService configurationService) : ObservableObject, IPreferencesService
public class PreferencesService : ObservableObject, IPreferencesService
{
private readonly IConfigurationService _configurationService = configurationService;
private readonly IConfigurationService _configurationService;
public event EventHandler<string> PreferenceChanged;
public PreferencesService(IConfigurationService configurationService)
{
_configurationService = configurationService;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
@@ -74,12 +79,6 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(IsMailListActionBarEnabled), value);
}
public bool IsShowActionLabelsEnabled
{
get => _configurationService.Get(nameof(IsShowActionLabelsEnabled), true);
set => SetPropertyAndSave(nameof(IsShowActionLabelsEnabled), value);
}
public bool IsShowSenderPicturesEnabled
{
get => _configurationService.Get(nameof(IsShowSenderPicturesEnabled), true);
@@ -176,18 +175,6 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value);
}
public bool IsGravatarEnabled
{
get => _configurationService.Get(nameof(IsGravatarEnabled), true);
set => SetPropertyAndSave(nameof(IsGravatarEnabled), value);
}
public bool IsFaviconEnabled
{
get => _configurationService.Get(nameof(IsFaviconEnabled), true);
set => SetPropertyAndSave(nameof(IsFaviconEnabled), value);
}
public Guid? StartupEntityId
{
get => _configurationService.Get<Guid?>(nameof(StartupEntityId), null);

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Printing;
using Windows.Data.Pdf;
using Windows.Graphics.Display;
using Windows.Graphics.Printing;
using Windows.Graphics.Printing.OptionDetails;
using Windows.Storage.Streams;
@@ -135,6 +136,11 @@ public class PrintService : IPrintService
private async void OnDocumentTaskOptionsChanged(CanvasPrintDocument sender, CanvasPrintTaskOptionsChangedEventArgs args)
{
System.Diagnostics.Debug.WriteLine("[PrintService] OnDocumentTaskOptionsChanged starting...");
DisplayInformation di = DisplayInformation.GetForCurrentView();
System.Diagnostics.Debug.WriteLine($"[PrintService] DisplayInformation.RawPixelsPerViewPixel: {di.RawPixelsPerViewPixel}");
System.Diagnostics.Debug.WriteLine($"[PrintService] DisplayInformation.LogicalDpi: {di.LogicalDpi}");
var deferral = args.GetDeferral();
try
@@ -142,6 +148,10 @@ public class PrintService : IPrintService
await LoadPDFPageBitmapsAsync(sender);
var pageDesc = args.PrintTaskOptions.GetPageDescription(1);
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.PageSize.Width (DIPs): {pageDesc.PageSize.Width}");
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.PageSize.Height (DIPs): {pageDesc.PageSize.Height}");
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.DpiX: {pageDesc.DpiX}");
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.DpiY: {pageDesc.DpiY}");
var newPageSize = pageDesc.PageSize.ToVector2();
if (pageSize == newPageSize && pageCount != -1)
@@ -176,6 +186,10 @@ public class PrintService : IPrintService
// Calculate the page count
bitmapCount = bitmaps.Count;
pageCount = (int)Math.Ceiling(bitmapCount / (double)bitmapsPerPage);
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated columns: {columns}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated rows: {rows}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated bitmapsPerPage: {bitmapsPerPage}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated pageCount: {pageCount}");
sender.SetPageCount((uint)pageCount);
// Set the preview page to the one that has the item that was currently displayed in the last preview
@@ -185,11 +199,31 @@ public class PrintService : IPrintService
{
deferral.Complete();
}
System.Diagnostics.Debug.WriteLine("[PrintService] OnDocumentTaskOptionsChanged finished.");
}
private async Task LoadPDFPageBitmapsAsync(CanvasPrintDocument sender)
{
System.Diagnostics.Debug.WriteLine("[PrintService] LoadPDFPageBitmapsAsync starting...");
DisplayInformation displayInfo = DisplayInformation.GetForCurrentView();
float rawPixelsPerViewPixel = (float)displayInfo.RawPixelsPerViewPixel;
if (rawPixelsPerViewPixel == 0)
{
System.Diagnostics.Debug.WriteLine($"[PrintService] Warning: RawPixelsPerViewPixel was 0, defaulting to 1.0f");
rawPixelsPerViewPixel = 1.0f; // Sanity check
}
System.Diagnostics.Debug.WriteLine($"[PrintService] DisplayInformation.RawPixelsPerViewPixel: {rawPixelsPerViewPixel}");
System.Diagnostics.Debug.WriteLine($"[PrintService] DisplayInformation.LogicalDpi: {displayInfo.LogicalDpi}");
float printerDpi = sender.Dpi;
if (printerDpi == 0)
{
System.Diagnostics.Debug.WriteLine($"[PrintService] Warning: sender.Dpi (printerDpi) was 0, defaulting to 96.0f");
printerDpi = 96.0f; // Sanity check
}
System.Diagnostics.Debug.WriteLine($"[PrintService] sender.Dpi (CanvasPrintDocument.Dpi, used as PrinterDPI): {printerDpi}");
ClearBitmaps();
bitmaps ??= new List<CanvasBitmap>();
@@ -197,10 +231,39 @@ public class PrintService : IPrintService
for (int i = 0; i < pdfDocument.PageCount; i++)
{
var page = pdfDocument.GetPage((uint)i);
var stream = new InMemoryRandomAccessStream();
await page.RenderToStreamAsync(stream);
var bitmap = await CanvasBitmap.LoadAsync(sender, stream);
bitmaps.Add(bitmap);
System.Diagnostics.Debug.WriteLine($"[PrintService] Processing page.Index: {page.Index}");
System.Diagnostics.Debug.WriteLine($"[PrintService] page.Dimensions.MediaBox.Width (PDF points): {page.Dimensions.MediaBox.Width}");
System.Diagnostics.Debug.WriteLine($"[PrintService] page.Dimensions.MediaBox.Height (PDF points): {page.Dimensions.MediaBox.Height}");
double pageWidthInPoints = page.Dimensions.MediaBox.Width;
double pageHeightInPoints = page.Dimensions.MediaBox.Height;
double pageWidthInInches = pageWidthInPoints / 72.0;
double pageHeightInInches = pageHeightInPoints / 72.0;
// Calculate the desired pixel dimensions of the bitmap based on printer DPI
uint targetPixelWidth = (uint)(pageWidthInInches * printerDpi);
uint targetPixelHeight = (uint)(pageHeightInInches * printerDpi);
// Calculate DestinationWidth/Height for PdfPageRenderOptions in DIPs
PdfPageRenderOptions options = new PdfPageRenderOptions();
options.DestinationWidth = (uint)(targetPixelWidth / rawPixelsPerViewPixel);
options.DestinationHeight = (uint)(targetPixelHeight / rawPixelsPerViewPixel);
System.Diagnostics.Debug.WriteLine($"[PrintService] Page {i}, Calculated PdfPageRenderOptions.DestinationWidth (DIPs): {options.DestinationWidth}, DestinationHeight (DIPs): {options.DestinationHeight}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Page {i}, TargetPixelWidth: {targetPixelWidth}, TargetPixelHeight: {targetPixelHeight}");
using (var stream = new InMemoryRandomAccessStream())
{
await page.RenderToStreamAsync(stream, options); // Use the options
var bitmap = await CanvasBitmap.LoadAsync(sender, stream);
System.Diagnostics.Debug.WriteLine($"[PrintService] bitmap.SizeInPixels.Width: {bitmap.SizeInPixels.Width}");
System.Diagnostics.Debug.WriteLine($"[PrintService] bitmap.SizeInPixels.Height: {bitmap.SizeInPixels.Height}");
System.Diagnostics.Debug.WriteLine($"[PrintService] bitmap.Dpi: {bitmap.Dpi}");
bitmaps.Add(bitmap);
}
}
largestBitmap = Vector2.Zero;
@@ -210,6 +273,7 @@ public class PrintService : IPrintService
largestBitmap.X = Math.Max(largestBitmap.X, (float)bitmap.Size.Width);
largestBitmap.Y = Math.Max(largestBitmap.Y, (float)bitmap.Size.Height);
}
System.Diagnostics.Debug.WriteLine("[PrintService] LoadPDFPageBitmapsAsync finished.");
}
@@ -262,4 +326,153 @@ public class PrintService : IPrintService
}
}
}
// --- START SIMULATION CODE ---
private async Task RunSimulationAsync(float simulatedRawPixelsPerViewPixel, string callingMethodName)
{
// --- Simulate LoadPDFPageBitmapsAsync ---
System.Diagnostics.Debug.WriteLine($"[PrintService] SIMULATING from {callingMethodName} for {simulatedRawPixelsPerViewPixel}");
System.Diagnostics.Debug.WriteLine("[PrintService] LoadPDFPageBitmapsAsync starting...");
// Mock DisplayInformation
float actualRawPixelsPerViewPixel = simulatedRawPixelsPerViewPixel; // Override
if (actualRawPixelsPerViewPixel == 0)
{
System.Diagnostics.Debug.WriteLine($"[PrintService] Warning: actualRawPixelsPerViewPixel was 0, defaulting to 1.0f");
actualRawPixelsPerViewPixel = 1.0f; // Sanity check
}
System.Diagnostics.Debug.WriteLine($"[PrintService] DisplayInformation.RawPixelsPerViewPixel: {actualRawPixelsPerViewPixel}");
// LogicalDpi is not directly used in calculations we are testing, so we can use a placeholder.
System.Diagnostics.Debug.WriteLine($"[PrintService] DisplayInformation.LogicalDpi: 96.0f (Simulated)");
// Mock CanvasPrintDocument sender for DPI
float printerDpi = 300.0f; // As per subtask
System.Diagnostics.Debug.WriteLine($"[PrintService] sender.Dpi (CanvasPrintDocument.Dpi, used as PrinterDPI): {printerDpi}");
// Clear any previous simulation state for bitmaps
// In a real scenario, ClearBitmaps() would be called. Here we just reset relevant fields.
this.bitmaps.Clear(); // Assuming 'bitmaps' is the List<CanvasBitmap>
this.largestBitmap = Vector2.Zero;
// Simulate pdfDocument.PageCount = 1 and loop once
int simulatedPageCount = 1;
for (int i = 0; i < simulatedPageCount; i++)
{
// Mock PdfPage
uint pageIndex = (uint)i;
double pageWidthInPoints = 612.0; // Letter size
double pageHeightInPoints = 792.0; // Letter size
System.Diagnostics.Debug.WriteLine($"[PrintService] Processing page.Index: {pageIndex}");
System.Diagnostics.Debug.WriteLine($"[PrintService] page.Dimensions.MediaBox.Width (PDF points): {pageWidthInPoints}");
System.Diagnostics.Debug.WriteLine($"[PrintService] page.Dimensions.MediaBox.Height (PDF points): {pageHeightInPoints}");
double pageWidthInInches = pageWidthInPoints / 72.0;
double pageHeightInInches = pageHeightInPoints / 72.0;
uint targetPixelWidth = (uint)(pageWidthInInches * printerDpi);
uint targetPixelHeight = (uint)(pageHeightInInches * printerDpi);
PdfPageRenderOptions options = new PdfPageRenderOptions();
options.DestinationWidth = (uint)(targetPixelWidth / actualRawPixelsPerViewPixel);
options.DestinationHeight = (uint)(targetPixelHeight / actualRawPixelsPerViewPixel);
System.Diagnostics.Debug.WriteLine($"[PrintService] Page {i}, Calculated PdfPageRenderOptions.DestinationWidth (DIPs): {options.DestinationWidth}, DestinationHeight (DIPs): {options.DestinationHeight}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Page {i}, TargetPixelWidth: {targetPixelWidth}, TargetPixelHeight: {targetPixelHeight}");
// Mock CanvasBitmap properties (as we can't actually render)
uint mockBitmapSizeInPixelsWidth = targetPixelWidth;
uint mockBitmapSizeInPixelsHeight = targetPixelHeight;
float mockBitmapDpi = printerDpi;
System.Diagnostics.Debug.WriteLine($"[PrintService] bitmap.SizeInPixels.Width: {mockBitmapSizeInPixelsWidth} (Simulated)");
System.Diagnostics.Debug.WriteLine($"[PrintService] bitmap.SizeInPixels.Height: {mockBitmapSizeInPixelsHeight} (Simulated)");
System.Diagnostics.Debug.WriteLine($"[PrintService] bitmap.Dpi: {mockBitmapDpi} (Simulated)");
// Simulate adding to bitmaps list and tracking largest
// We don't add a real CanvasBitmap, just update what's needed for OnDocumentTaskOptionsChanged
this.largestBitmap.X = Math.Max(this.largestBitmap.X, mockBitmapSizeInPixelsWidth);
this.largestBitmap.Y = Math.Max(this.largestBitmap.Y, mockBitmapSizeInPixelsHeight);
// bitmaps.Add(null); // Not adding real bitmaps
}
this.bitmapCount = simulatedPageCount; // Set based on simulation
System.Diagnostics.Debug.WriteLine("[PrintService] LoadPDFPageBitmapsAsync finished.");
// --- Simulate OnDocumentTaskOptionsChanged ---
System.Diagnostics.Debug.WriteLine("[PrintService] OnDocumentTaskOptionsChanged starting...");
// DisplayInformation logs already done in LoadPDFPageBitmapsAsync simulation part
// Mock PageDescription (from PrintTaskOptions)
// For Letter paper (8.5x11 inches) at 300 DPI:
// Pixel size = (8.5*300) x (11*300) = 2550 x 3300 pixels
// DIP size = (PixelSize / rawPixelsPerViewPixel)
float pageDescPageSizeWidth = (2550.0f / actualRawPixelsPerViewPixel);
float pageDescPageSizeHeight = (3300.0f / actualRawPixelsPerViewPixel);
float pageDescDpiX = 300.0f;
float pageDescDpiY = 300.0f;
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.PageSize.Width (DIPs): {pageDescPageSizeWidth} (Simulated)");
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.PageSize.Height (DIPs): {pageDescPageSizeHeight} (Simulated)");
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.DpiX: {pageDescDpiX} (Simulated)");
System.Diagnostics.Debug.WriteLine($"[PrintService] pageDesc.DpiY: {pageDescDpiY} (Simulated)");
this.pageSize = new Vector2(pageDescPageSizeWidth, pageDescPageSizeHeight);
// sender.InvalidatePreview(); // Cannot call
// Calculate the new layout
var printablePageSize = this.pageSize * 0.9f; // Assuming default behavior
this.imagePadding = new Vector2(64, 64); // Default from class
this.cellSize = this.largestBitmap + this.imagePadding;
var cellsPerPage = printablePageSize / this.cellSize;
this.columns = Math.Max(1, (int)Math.Floor(cellsPerPage.X));
this.rows = Math.Max(1, (int)Math.Floor(cellsPerPage.Y));
this.bitmapsPerPage = this.columns * this.rows;
this.pageCount = (int)Math.Ceiling(this.bitmapCount / (double)this.bitmapsPerPage);
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated columns: {this.columns}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated rows: {this.rows}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated bitmapsPerPage: {this.bitmapsPerPage}");
System.Diagnostics.Debug.WriteLine($"[PrintService] Calculated pageCount: {this.pageCount}");
// sender.SetPageCount((uint)this.pageCount); // Cannot call
System.Diagnostics.Debug.WriteLine("[PrintService] OnDocumentTaskOptionsChanged finished.");
System.Diagnostics.Debug.WriteLine($"[PrintService] SIMULATION END for {simulatedRawPixelsPerViewPixel}");
System.Diagnostics.Debug.WriteLine("---"); // Separator
}
public async Task SimulatePrintScalingAsync()
{
// Temporarily store and clear global state that might interfere
var originalBitmaps = new List<CanvasBitmap>(this.bitmaps);
var originalLargestBitmap = this.largestBitmap;
var originalPageSize = this.pageSize;
var originalCellSize = this.cellSize;
int originalBitmapCount = this.bitmapCount;
int originalColumns = this.columns;
int originalRows = this.rows;
int originalBitmapsPerPage = this.bitmapsPerPage;
int originalPageCount = this.pageCount;
await RunSimulationAsync(1.0f, nameof(SimulatePrintScalingAsync));
await RunSimulationAsync(1.25f, nameof(SimulatePrintScalingAsync));
await RunSimulationAsync(1.5f, nameof(SimulatePrintScalingAsync));
// Restore original state if necessary, though for this task it's just about logs
this.bitmaps = originalBitmaps;
this.largestBitmap = originalLargestBitmap;
this.pageSize = originalPageSize;
this.cellSize = originalCellSize;
this.bitmapCount = originalBitmapCount;
this.columns = originalColumns;
this.rows = originalRows;
this.bitmapsPerPage = originalBitmapsPerPage;
this.pageCount = originalPageCount;
}
// --- END SIMULATION CODE ---
}

View File

@@ -1,198 +1,63 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Mail;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Gravatar;
using Windows.Networking.Connectivity;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Messaging.UI;
using Wino.Services;
namespace Wino.Core.UWP.Services;
public class ThumbnailService(IPreferencesService preferencesService, IDatabaseService databaseService) : IThumbnailService
public static class ThumbnailService
{
private readonly IPreferencesService _preferencesService = preferencesService;
private readonly IDatabaseService _databaseService = databaseService;
private static readonly HttpClient _httpClient = new();
private bool _isInitialized = false;
private ConcurrentDictionary<string, (string graviton, string favicon)> _cache;
private readonly ConcurrentDictionary<string, Task> _requests = [];
private static readonly List<string> _excludedFaviconDomains = [
"gmail.com",
"outlook.com",
"hotmail.com",
"live.com",
"yahoo.com",
"icloud.com",
"aol.com",
"protonmail.com",
"zoho.com",
"mail.com",
"gmx.com",
"yandex.com",
"yandex.ru",
"tutanota.com",
"mail.ru",
"rediffmail.com"
];
public async ValueTask<string> GetThumbnailAsync(string email, bool awaitLoad = false)
private static string[] knownCompanies = new string[]
{
if (string.IsNullOrWhiteSpace(email))
return null;
"microsoft.com", "apple.com", "google.com", "steampowered.com", "airbnb.com", "youtube.com", "uber.com"
};
if (!_preferencesService.IsShowSenderPicturesEnabled)
return null;
public static bool IsKnown(string mailHost) => !string.IsNullOrEmpty(mailHost) && knownCompanies.Contains(mailHost);
if (!_isInitialized)
{
var thumbnailsList = await _databaseService.Connection.Table<Thumbnail>().ToListAsync();
_cache = new ConcurrentDictionary<string, (string graviton, string favicon)>(
thumbnailsList.ToDictionary(x => x.Domain, x => (x.Gravatar, x.Favicon)));
_isInitialized = true;
}
var sanitizedEmail = email.Trim().ToLowerInvariant();
var (gravatar, favicon) = await GetThumbnailInternal(sanitizedEmail, awaitLoad);
if (_preferencesService.IsGravatarEnabled && !string.IsNullOrEmpty(gravatar))
{
return gravatar;
}
if (_preferencesService.IsFaviconEnabled && !string.IsNullOrEmpty(favicon))
{
return favicon;
}
return null;
}
public async Task ClearCache()
public static string GetHost(string address)
{
_cache?.Clear();
_requests.Clear();
await _databaseService.Connection.DeleteAllAsync<Thumbnail>();
}
if (string.IsNullOrEmpty(address))
return string.Empty;
private async ValueTask<(string gravatar, string favicon)> GetThumbnailInternal(string email, bool awaitLoad)
{
if (_cache.TryGetValue(email, out var cached))
return cached;
// No network available, skip fetching Gravatar
// Do not cache it, since network can be available later
bool isInternetAvailable = GetIsInternetAvailable();
if (!isInternetAvailable)
return default;
if (!_requests.TryGetValue(email, out var request))
if (address.Contains('@'))
{
request = Task.Run(() => RequestNewThumbnail(email));
_requests[email] = request;
}
var splitted = address.Split('@');
if (awaitLoad)
{
await request;
_cache.TryGetValue(email, out cached);
return cached;
}
return default;
static bool GetIsInternetAvailable()
{
var connection = NetworkInformation.GetInternetConnectionProfile();
return connection != null && connection.GetNetworkConnectivityLevel() == NetworkConnectivityLevel.InternetAccess;
}
}
private async Task RequestNewThumbnail(string email)
{
var gravatarBase64 = await GetGravatarBase64(email);
var faviconBase64 = await GetFaviconBase64(email);
await _databaseService.Connection.InsertOrReplaceAsync(new Thumbnail
{
Domain = email,
Gravatar = gravatarBase64,
Favicon = faviconBase64,
LastUpdated = DateTime.UtcNow
});
_ = _cache.TryAdd(email, (gravatarBase64, faviconBase64));
WeakReferenceMessenger.Default.Send(new ThumbnailAdded(email));
}
private static async Task<string> GetGravatarBase64(string email)
{
try
{
var gravatarUrl = GravatarHelper.GetAvatarUrl(
email,
size: 128,
defaultValue: GravatarAvatarDefault.Blank,
withFileExtension: false).ToString().Replace("d=blank", "d=404");
var response = await _httpClient.GetAsync(gravatarUrl);
if (response.IsSuccessStatusCode)
if (splitted.Length >= 2 && !string.IsNullOrEmpty(splitted[1]))
{
var bytes = response.Content.ReadAsByteArrayAsync().Result;
return Convert.ToBase64String(bytes);
try
{
return new MailAddress(address).Host;
}
catch (Exception)
{
// TODO: Exceptions are ignored for now.
}
}
}
catch { }
return null;
}
private static async Task<string> GetFaviconBase64(string email)
{
try
{
var host = GetHost(email);
if (string.IsNullOrEmpty(host))
return null;
// Do not fetch favicon for specific default domains of major platforms
if (_excludedFaviconDomains.Contains(host, StringComparer.OrdinalIgnoreCase))
return null;
var primaryDomain = string.Join('.', host.Split('.')[^2..]);
var googleFaviconUrl = $"https://www.google.com/s2/favicons?sz=128&domain_url={primaryDomain}";
var response = await _httpClient.GetAsync(googleFaviconUrl);
if (response.IsSuccessStatusCode)
{
var bytes = response.Content.ReadAsByteArrayAsync().Result;
return Convert.ToBase64String(bytes);
}
}
catch { }
return null;
}
private static string GetHost(string email)
{
if (!string.IsNullOrEmpty(email) && email.Contains('@'))
{
var split = email.Split('@');
if (split.Length >= 2 && !string.IsNullOrEmpty(split[1]))
{
try { return new MailAddress(email).Host; } catch { }
}
}
return string.Empty;
}
public static Tuple<bool, string> CheckIsKnown(string host)
{
// Check known hosts.
// Apply company logo if available.
try
{
var last = host.Split('.');
if (last.Length > 2)
host = $"{last[last.Length - 2]}.{last[last.Length - 1]}";
}
catch (Exception)
{
return new Tuple<bool, string>(false, host);
}
return new Tuple<bool, string>(IsKnown(host), host);
}
public static string GetKnownHostImage(string host)
=> $"ms-appx:///Assets/Thumbnails/{host}.png";
}

View File

@@ -85,7 +85,6 @@
<Content Include="BackgroundImages\Snowflake.jpg" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="gravatar-dotnet" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="Microsoft.UI.Xaml" />
<PackageReference Include="CommunityToolkit.Common" />

View File

@@ -40,7 +40,6 @@ public abstract class WinoApplication : Application, IRecipient<LanguageChanged>
protected IWinoServerConnectionManager<AppServiceConnection> AppServiceConnectionManager { get; }
public IThemeService ThemeService { get; }
public IUnderlyingThemeService UnderlyingThemeService { get; }
public IThumbnailService ThumbnailService { get; }
protected IDatabaseService DatabaseService { get; }
protected ITranslationService TranslationService { get; }
@@ -65,7 +64,6 @@ public abstract class WinoApplication : Application, IRecipient<LanguageChanged>
DatabaseService = Services.GetService<IDatabaseService>();
TranslationService = Services.GetService<ITranslationService>();
UnderlyingThemeService = Services.GetService<IUnderlyingThemeService>();
ThumbnailService = Services.GetService<IThumbnailService>();
// Make sure the paths are setup on app start.
AppConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path;

View File

@@ -5,6 +5,7 @@ using System.Web;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Gmail.v1.Data;
using MimeKit;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -178,11 +179,6 @@ public static class GoogleIntegratorExtensions
Id = Guid.NewGuid(),
TimeZone = calendarListEntry.TimeZone,
IsPrimary = calendarListEntry.Primary.GetValueOrDefault(),
Description = calendarListEntry.Description,
AccessRole = calendarListEntry.AccessRole,
CreatedDate = DateTime.UtcNow,
LastSyncTime = DateTime.UtcNow,
Location = calendarListEntry.Location,
};
// Bg color must present. Generate one if doesnt exists.
@@ -194,121 +190,42 @@ public static class GoogleIntegratorExtensions
return calendar;
}
public static CalendarItem MapGoogleEventToCalendarEvent(this Event googleEvent, AccountCalendar calendar)
public static DateTimeOffset? GetEventDateTimeOffset(EventDateTime calendarEvent)
{
var calendarEvent = new CalendarItem
if (calendarEvent != null)
{
RemoteEventId = googleEvent.Id,
CalendarId = calendar.Id, // Use internal Guid
Title = googleEvent.Summary ?? string.Empty,
Description = googleEvent.Description,
Location = googleEvent.Location,
Status = googleEvent.Status,
RecurringEventId = googleEvent.RecurringEventId
};
// Handle start and end times
if (googleEvent.Start != null)
{
if (googleEvent.Start.Date != null)
if (calendarEvent.DateTimeDateTimeOffset != null)
{
calendarEvent.IsAllDay = true;
calendarEvent.StartDateTime = DateTime.Parse(googleEvent.Start.Date);
return calendarEvent.DateTimeDateTimeOffset.Value;
}
else if (googleEvent.Start.DateTimeDateTimeOffset.HasValue)
else if (calendarEvent.Date != null)
{
calendarEvent.IsAllDay = false;
calendarEvent.StartDateTime = googleEvent.Start.DateTimeDateTimeOffset.Value.DateTime;
calendarEvent.TimeZone = googleEvent.Start.TimeZone;
if (DateTime.TryParse(calendarEvent.Date, out DateTime eventDateTime))
{
// Date-only events are treated as UTC midnight
return new DateTimeOffset(eventDateTime, TimeSpan.Zero);
}
else
{
throw new Exception("Invalid date format in Google Calendar event date.");
}
}
}
if (googleEvent.End != null)
{
if (googleEvent.End.Date != null)
{
calendarEvent.EndDateTime = DateTime.Parse(googleEvent.End.Date);
}
else if (googleEvent.End.DateTimeDateTimeOffset.HasValue)
{
calendarEvent.EndDateTime = googleEvent.End.DateTimeDateTimeOffset.Value.DateTime;
}
}
// Handle recurrence rules
if (googleEvent.Recurrence != null && googleEvent.Recurrence.Count > 0)
{
calendarEvent.RecurrenceRules = string.Join(";", googleEvent.Recurrence);
}
// Handle organizer
if (googleEvent.Organizer != null)
{
calendarEvent.OrganizerDisplayName = googleEvent.Organizer.DisplayName;
calendarEvent.OrganizerEmail = googleEvent.Organizer.Email;
}
// Handle timestamps
if (googleEvent.CreatedDateTimeOffset.HasValue)
{
calendarEvent.CreatedDate = googleEvent.CreatedDateTimeOffset.Value.DateTime;
}
if (googleEvent.UpdatedDateTimeOffset.HasValue)
{
calendarEvent.LastModified = googleEvent.UpdatedDateTimeOffset.Value.DateTime;
}
// Handle original start time for recurring event instances
if (googleEvent.OriginalStartTime != null)
{
if (googleEvent.OriginalStartTime.Date != null)
{
calendarEvent.OriginalStartTime = googleEvent.OriginalStartTime.Date;
}
else if (googleEvent.OriginalStartTime.DateTimeDateTimeOffset.HasValue)
{
calendarEvent.OriginalStartTime = googleEvent.OriginalStartTime.DateTimeDateTimeOffset.Value.ToString("O");
}
}
// Automatically determine the calendar item type based on event properties
calendarEvent.DetermineItemType();
return calendarEvent;
return null;
}
/// <summary>
/// Converts a Google Calendar API response status string to AttendeeResponseStatus enum
/// RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545.
/// </summary>
/// <param name="googleStatus">The status string from Google Calendar API</param>
/// <returns>Corresponding AttendeeResponseStatus enum value</returns>
public static AttendeeResponseStatus FromGoogleStatus(string? googleStatus)
/// <returns>___ separated lines.</returns>
public static string GetRecurrenceString(this Event calendarEvent)
{
return googleStatus?.ToLowerInvariant() switch
if (calendarEvent == null || calendarEvent.Recurrence == null || !calendarEvent.Recurrence.Any())
{
"accepted" => AttendeeResponseStatus.Accepted,
"declined" => AttendeeResponseStatus.Declined,
"tentative" => AttendeeResponseStatus.Tentative,
"needsaction" => AttendeeResponseStatus.NeedsAction,
_ => AttendeeResponseStatus.NeedsAction
};
}
return null;
}
/// <summary>
/// Converts an AttendeeResponseStatus enum to Google Calendar API response status string
/// </summary>
/// <param name="status">The AttendeeResponseStatus enum value</param>
/// <returns>Corresponding Google Calendar API status string</returns>
public static string ToGoogleStatus(AttendeeResponseStatus status)
{
return status switch
{
AttendeeResponseStatus.Accepted => "accepted",
AttendeeResponseStatus.Declined => "declined",
AttendeeResponseStatus.Tentative => "tentative",
AttendeeResponseStatus.NeedsAction => "needsAction",
_ => "needsAction"
};
return string.Join(Constants.CalendarEventRecurrenceRuleSeperator, calendarEvent.Recurrence);
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Graph.Models;
using MimeKit;
using Wino.Core.Domain.Entities.Calendar;
@@ -126,26 +127,125 @@ public static class OutlookIntegratorExtensions
return calendar;
}
/// <summary>
/// Converts Outlook response status to our enum
/// </summary>
/// <param name="outlookResponse">Outlook response type</param>
/// <returns>AttendeeResponseStatus enum value</returns>
public static AttendeeResponseStatus ConvertOutlookResponseStatus(this Microsoft.Graph.Models.ResponseType? outlookResponse)
private static string GetRfc5545DayOfWeek(DayOfWeekObject dayOfWeek)
{
return outlookResponse switch
return dayOfWeek switch
{
Microsoft.Graph.Models.ResponseType.Accepted => AttendeeResponseStatus.Accepted,
Microsoft.Graph.Models.ResponseType.Declined => AttendeeResponseStatus.Declined,
Microsoft.Graph.Models.ResponseType.TentativelyAccepted => AttendeeResponseStatus.Tentative,
Microsoft.Graph.Models.ResponseType.None => AttendeeResponseStatus.NeedsAction,
Microsoft.Graph.Models.ResponseType.NotResponded => AttendeeResponseStatus.NeedsAction,
_ => AttendeeResponseStatus.NeedsAction
DayOfWeekObject.Monday => "MO",
DayOfWeekObject.Tuesday => "TU",
DayOfWeekObject.Wednesday => "WE",
DayOfWeekObject.Thursday => "TH",
DayOfWeekObject.Friday => "FR",
DayOfWeekObject.Saturday => "SA",
DayOfWeekObject.Sunday => "SU",
_ => throw new ArgumentOutOfRangeException(nameof(dayOfWeek), dayOfWeek, null)
};
}
public static string ToRfc5545RecurrenceString(this PatternedRecurrence recurrence)
{
if (recurrence == null || recurrence.Pattern == null)
throw new ArgumentNullException(nameof(recurrence), "PatternedRecurrence or its Pattern cannot be null.");
var ruleBuilder = new StringBuilder("RRULE:");
var pattern = recurrence.Pattern;
// Frequency
switch (pattern.Type)
{
case RecurrencePatternType.Daily:
ruleBuilder.Append("FREQ=DAILY;");
break;
case RecurrencePatternType.Weekly:
ruleBuilder.Append("FREQ=WEEKLY;");
break;
case RecurrencePatternType.AbsoluteMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.AbsoluteYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
case RecurrencePatternType.RelativeMonthly:
ruleBuilder.Append("FREQ=MONTHLY;");
break;
case RecurrencePatternType.RelativeYearly:
ruleBuilder.Append("FREQ=YEARLY;");
break;
default:
throw new NotSupportedException($"Unsupported recurrence pattern type: {pattern.Type}");
}
// Interval
if (pattern.Interval > 0)
ruleBuilder.Append($"INTERVAL={pattern.Interval};");
// Days of Week
if (pattern.DaysOfWeek?.Any() == true)
{
var days = string.Join(",", pattern.DaysOfWeek.Select(day => day.ToString().ToUpperInvariant().Substring(0, 2)));
ruleBuilder.Append($"BYDAY={days};");
}
// Day of Month (BYMONTHDAY)
if (pattern.Type == RecurrencePatternType.AbsoluteMonthly || pattern.Type == RecurrencePatternType.AbsoluteYearly)
{
if (pattern.DayOfMonth <= 0)
throw new ArgumentException("DayOfMonth must be greater than 0 for absoluteMonthly or absoluteYearly patterns.");
ruleBuilder.Append($"BYMONTHDAY={pattern.DayOfMonth};");
}
// Month (BYMONTH)
if (pattern.Type == RecurrencePatternType.AbsoluteYearly || pattern.Type == RecurrencePatternType.RelativeYearly)
{
if (pattern.Month <= 0)
throw new ArgumentException("Month must be greater than 0 for absoluteYearly or relativeYearly patterns.");
ruleBuilder.Append($"BYMONTH={pattern.Month};");
}
// Count or Until
if (recurrence.Range != null)
{
if (recurrence.Range.Type == RecurrenceRangeType.EndDate && recurrence.Range.EndDate != null)
{
ruleBuilder.Append($"UNTIL={recurrence.Range.EndDate.Value:yyyyMMddTHHmmssZ};");
}
else if (recurrence.Range.Type == RecurrenceRangeType.Numbered && recurrence.Range.NumberOfOccurrences.HasValue)
{
ruleBuilder.Append($"COUNT={recurrence.Range.NumberOfOccurrences.Value};");
}
}
// Remove trailing semicolon
return ruleBuilder.ToString().TrimEnd(';');
}
public static DateTimeOffset GetDateTimeOffsetFromDateTimeTimeZone(DateTimeTimeZone dateTimeTimeZone)
{
if (dateTimeTimeZone == null || string.IsNullOrEmpty(dateTimeTimeZone.DateTime) || string.IsNullOrEmpty(dateTimeTimeZone.TimeZone))
{
throw new ArgumentException("DateTimeTimeZone is null or empty.");
}
try
{
// Parse the DateTime string
if (DateTime.TryParse(dateTimeTimeZone.DateTime, out DateTime parsedDateTime))
{
// Get TimeZoneInfo to get the offset
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(dateTimeTimeZone.TimeZone);
TimeSpan offset = timeZoneInfo.GetUtcOffset(parsedDateTime);
return new DateTimeOffset(parsedDateTime, offset);
}
else
throw new ArgumentException("DateTime string is not in a valid format.");
}
catch (Exception)
{
throw;
}
}
private static AttendeeStatus GetAttendeeStatus(ResponseType? responseType)
{
@@ -161,6 +261,24 @@ public static class OutlookIntegratorExtensions
};
}
public static CalendarEventAttendee CreateAttendee(this Attendee attendee, Guid calendarItemId)
{
bool isOrganizer = attendee?.Status?.Response == ResponseType.Organizer;
var eventAttendee = new CalendarEventAttendee()
{
CalendarItemId = calendarItemId,
Id = Guid.NewGuid(),
Email = attendee.EmailAddress?.Address,
Name = attendee.EmailAddress?.Name,
AttendenceStatus = GetAttendeeStatus(attendee.Status.Response),
IsOrganizer = isOrganizer,
IsOptionalAttendee = attendee.Type == AttendeeType.Optional,
};
return eventAttendee;
}
#region Mime to Outlook Message Helpers
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)

View File

@@ -21,7 +21,7 @@ namespace Wino.Core.Integration.Processors;
/// <see cref="IGmailChangeProcessor"/>, <see cref="IOutlookChangeProcessor"/> and <see cref="IImapChangeProcessor"/>
/// None of the synchronizers can directly change anything in the database.
/// </summary>
public interface IDefaultChangeProcessor : ICalendarServiceEx
public interface IDefaultChangeProcessor
{
Task UpdateAccountAsync(MailAccount account);
// Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
@@ -113,14 +113,13 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor, ICalendarServiceEx
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
{
protected IMailService MailService = mailService;
protected ICalendarService CalendarService = calendarService;
protected IFolderService FolderService = folderService;
protected IAccountService AccountService = accountService;
private readonly ICalendarServiceEx _calendarServiceEx = calendarServiceEx;
private readonly IMimeFileService _mimeFileService = mimeFileService;
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
@@ -209,234 +208,4 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);
// TODO: Normalize this shit. Not everything needs to be exposed for processor.
#region ICalendarServiceEx
public Task<int> ClearAllCalendarEventAttendeesAsync()
{
return _calendarServiceEx.ClearAllCalendarEventAttendeesAsync();
}
public Task<int> ClearAllCalendarsAsync()
{
return _calendarServiceEx.ClearAllCalendarsAsync();
}
public Task<int> ClearAllDataAsync()
{
return _calendarServiceEx.ClearAllDataAsync();
}
public Task<int> ClearAllEventsAsync()
{
return _calendarServiceEx.ClearAllEventsAsync();
}
public Task<int> DeleteCalendarAsync(string remoteCalendarId)
{
return _calendarServiceEx.DeleteCalendarAsync(remoteCalendarId);
}
public Task<int> DeleteCalendarEventAttendeesForEventAsync(Guid eventId)
{
return _calendarServiceEx.DeleteCalendarEventAttendeesForEventAsync(eventId);
}
public Task<int> DeleteEventAsync(string remoteEventId)
{
return _calendarServiceEx.DeleteEventAsync(remoteEventId);
}
public Task<List<CalendarEventAttendee>> GetAllCalendarEventAttendeesAsync()
{
return _calendarServiceEx.GetAllCalendarEventAttendeesAsync();
}
public Task<List<AccountCalendar>> GetAllCalendarsAsync()
{
return _calendarServiceEx.GetAllCalendarsAsync();
}
public Task<List<CalendarItem>> GetAllDayEventsAsync()
{
return _calendarServiceEx.GetAllDayEventsAsync();
}
public Task<List<CalendarItem>> GetAllEventsAsync()
{
return _calendarServiceEx.GetAllEventsAsync();
}
public Task<List<CalendarItem>> GetAllEventsIncludingDeletedAsync()
{
return _calendarServiceEx.GetAllEventsIncludingDeletedAsync();
}
public Task<List<CalendarItem>> GetAllRecurringEventsByTypeAsync()
{
return _calendarServiceEx.GetAllRecurringEventsByTypeAsync();
}
public Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId)
{
return _calendarServiceEx.GetCalendarByRemoteIdAsync(remoteCalendarId);
}
public Task<Dictionary<AttendeeResponseStatus, int>> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId)
{
return _calendarServiceEx.GetCalendarEventAttendeeResponseCountsAsync(eventId);
}
public Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventAsync(Guid eventId)
{
return _calendarServiceEx.GetCalendarEventAttendeesForEventAsync(eventId);
}
public Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId)
{
return _calendarServiceEx.GetCalendarEventAttendeesForEventByRemoteIdAsync(remoteEventId);
}
public Task<string> GetCalendarSyncTokenAsync(string calendarId)
{
return _calendarServiceEx.GetCalendarSyncTokenAsync(calendarId);
}
public Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId)
{
return _calendarServiceEx.GetEventByRemoteIdAsync(remoteEventId);
}
public Task<List<CalendarItem>> GetEventsByItemTypeAsync(CalendarItemType itemType)
{
return _calendarServiceEx.GetEventsByItemTypeAsync(itemType);
}
public Task<List<CalendarItem>> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes)
{
return _calendarServiceEx.GetEventsByItemTypesAsync(itemTypes);
}
public Task<List<CalendarItem>> GetEventsByremoteCalendarIdAsync(string remoteCalendarId)
{
return _calendarServiceEx.GetEventsByremoteCalendarIdAsync(remoteCalendarId);
}
public Task<List<CalendarItem>> GetEventsForCalendarAsync(Guid calendarId)
{
return _calendarServiceEx.GetEventsForCalendarAsync(calendarId);
}
public Task<List<CalendarItem>> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
{
return _calendarServiceEx.GetEventsInDateRangeAsync(startDate, endDate);
}
public Task<List<CalendarItem>> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime)
{
return _calendarServiceEx.GetEventsSinceLastSyncAsync(lastSyncTime);
}
public Task<Dictionary<CalendarItemType, int>> GetEventStatsByItemTypeAsync()
{
return _calendarServiceEx.GetEventStatsByItemTypeAsync();
}
public Task<List<CalendarItem>> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
{
return _calendarServiceEx.GetExpandedEventsInDateRangeAsync(startDate, endDate);
}
public Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar)
{
return _calendarServiceEx.GetExpandedEventsInDateRangeWithExceptionsAsync(startDate, endDate, calendar);
}
public Task<DateTime?> GetLastSyncTimeAsync(string calendarId)
{
return _calendarServiceEx.GetLastSyncTimeAsync(calendarId);
}
public Task<List<CalendarItem>> GetMultiDayEventsAsync()
{
return _calendarServiceEx.GetMultiDayEventsAsync();
}
public Task<List<CalendarItem>> GetRecurringEventsAsync()
{
return _calendarServiceEx.GetRecurringEventsAsync();
}
public Task<int> HardDeleteEventAsync(string remoteEventId)
{
return _calendarServiceEx.HardDeleteEventAsync(remoteEventId);
}
public Task<int> InsertCalendarAsync(AccountCalendar calendar)
{
return _calendarServiceEx.InsertCalendarAsync(calendar);
}
public Task<int> InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
{
return _calendarServiceEx.InsertCalendarEventAttendeeAsync(calendareventattendee);
}
public Task<int> InsertEventAsync(CalendarItem calendarItem)
{
return _calendarServiceEx.InsertEventAsync(calendarItem);
}
public Task<int> MarkEventAsDeletedAsync(string remoteEventId, string remoteCalendarId)
{
return _calendarServiceEx.MarkEventAsDeletedAsync(remoteEventId, remoteCalendarId);
}
public Task<int> SyncCalendarEventAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> calendareventattendees)
{
return _calendarServiceEx.SyncCalendarEventAttendeesForEventAsync(eventId, calendareventattendees);
}
public Task<int> UpdateAllEventItemTypesAsync()
{
return _calendarServiceEx.UpdateAllEventItemTypesAsync();
}
public Task<int> UpdateCalendarAsync(AccountCalendar calendar)
{
return _calendarServiceEx.UpdateCalendarAsync(calendar);
}
public Task<int> UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
{
return _calendarServiceEx.UpdateCalendarEventAttendeeAsync(calendareventattendee);
}
public Task<int> UpdateCalendarSyncTokenAsync(string calendarId, string syncToken)
{
return _calendarServiceEx.UpdateCalendarSyncTokenAsync(calendarId, syncToken);
}
public Task<int> UpdateEventAsync(CalendarItem calendarItem)
{
return _calendarServiceEx.UpdateEventAsync(calendarItem);
}
public Task<int> UpsertCalendarAsync(AccountCalendar calendar)
{
return _calendarServiceEx.UpsertCalendarAsync(calendar);
}
public Task<int> UpsertEventAsync(CalendarItem calendarItem)
{
return _calendarServiceEx.UpsertEventAsync(calendarItem);
}
public Task<int> SyncAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> attendees)
{
return _calendarServiceEx.SyncAttendeesForEventAsync(eventId, attendees);
}
#endregion
}

View File

@@ -1,13 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Google.Apis.Calendar.v3.Data;
using Serilog;
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.MailItem;
using Wino.Core.Extensions;
using Wino.Services;
using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee;
using CalendarItem = Wino.Core.Domain.Entities.Calendar.CalendarItem;
namespace Wino.Core.Integration.Processors;
@@ -18,8 +23,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
IMailService mailService,
ICalendarService calendarService,
IAccountService accountService,
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
{
}
@@ -32,7 +36,224 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// TODO:
var status = calendarEvent.Status;
var recurringEventId = calendarEvent.RecurringEventId;
// 1. Canceled exceptions of recurred events are only guaranteed to have recurringEventId, Id and start time.
// 2. Updated exceptions of recurred events have different Id, but recurringEventId is the same as parent.
// Check if we have this event before.
var existingCalendarItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
if (existingCalendarItem == null)
{
CalendarItem parentRecurringEvent = null;
// Manage the recurring event id.
if (!string.IsNullOrEmpty(recurringEventId))
{
parentRecurringEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, recurringEventId).ConfigureAwait(false);
if (parentRecurringEvent == null)
{
Log.Information($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// We don't have this event yet. Create a new one.
var eventStartDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.Start);
var eventEndDateTimeOffset = GoogleIntegratorExtensions.GetEventDateTimeOffset(calendarEvent.End);
double totalDurationInSeconds = 0;
if (eventStartDateTimeOffset != null && eventEndDateTimeOffset != null)
{
totalDurationInSeconds = (eventEndDateTimeOffset.Value - eventStartDateTimeOffset.Value).TotalSeconds;
}
CalendarItem calendarItem = null;
if (parentRecurringEvent != null)
{
// Exceptions of parent events might not have all the fields populated.
// We must use the parent event's data for fields that don't exists.
// Update duration if it's not populated.
if (totalDurationInSeconds == 0)
{
totalDurationInSeconds = parentRecurringEvent.DurationInSeconds;
}
var organizerMail = GetOrganizerEmail(calendarEvent, organizerAccount);
var organizerName = GetOrganizerName(calendarEvent, organizerAccount);
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description ?? parentRecurringEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset?.Offset ?? parentRecurringEvent.EndDateOffset,
DurationInSeconds = totalDurationInSeconds,
Location = string.IsNullOrEmpty(calendarEvent.Location) ? parentRecurringEvent.Location : calendarEvent.Location,
// Leave it empty if it's not populated.
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent) == null ? string.Empty : GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = string.IsNullOrEmpty(calendarEvent.Summary) ? parentRecurringEvent.Title : calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = string.IsNullOrEmpty(calendarEvent.Visibility) ? parentRecurringEvent.Visibility : GetVisibility(calendarEvent.Visibility),
HtmlLink = string.IsNullOrEmpty(calendarEvent.HtmlLink) ? parentRecurringEvent.HtmlLink : calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = string.IsNullOrEmpty(organizerName) ? parentRecurringEvent.OrganizerDisplayName : organizerName,
OrganizerEmail = string.IsNullOrEmpty(organizerMail) ? parentRecurringEvent.OrganizerEmail : organizerMail
};
}
else
{
// This is a parent event creation.
// Start-End dates are guaranteed to be populated.
if (eventStartDateTimeOffset == null || eventEndDateTimeOffset == null)
{
Log.Error("Failed to create parent event because either start or end date is not specified.");
return;
}
calendarItem = new CalendarItem()
{
CalendarId = assignedCalendar.Id,
CreatedAt = DateTimeOffset.UtcNow,
Description = calendarEvent.Description,
Id = Guid.NewGuid(),
StartDate = eventStartDateTimeOffset.Value.DateTime,
StartDateOffset = eventStartDateTimeOffset.Value.Offset,
EndDateOffset = eventEndDateTimeOffset.Value.Offset,
DurationInSeconds = totalDurationInSeconds,
Location = calendarEvent.Location,
Recurrence = GoogleIntegratorExtensions.GetRecurrenceString(calendarEvent),
Status = GetStatus(calendarEvent.Status),
Title = calendarEvent.Summary,
UpdatedAt = DateTimeOffset.UtcNow,
Visibility = GetVisibility(calendarEvent.Visibility),
HtmlLink = calendarEvent.HtmlLink,
RemoteEventId = calendarEvent.Id,
IsLocked = calendarEvent.Locked.GetValueOrDefault(),
OrganizerDisplayName = GetOrganizerName(calendarEvent, organizerAccount),
OrganizerEmail = GetOrganizerEmail(calendarEvent, organizerAccount)
};
}
// Hide canceled events.
calendarItem.IsHidden = calendarItem.Status == CalendarItemStatus.Cancelled;
// Manage the recurring event id.
if (parentRecurringEvent != null)
{
calendarItem.RecurringCalendarItemId = parentRecurringEvent.Id;
}
Debug.WriteLine($"({assignedCalendar.Name}) {calendarItem.Title}, Start: {calendarItem.StartDate.ToString("f")}, End: {calendarItem.EndDate.ToString("f")}");
// Attendees
var attendees = new List<CalendarEventAttendee>();
if (calendarEvent.Attendees == null)
{
// Self-only event.
attendees.Add(new CalendarEventAttendee()
{
CalendarItemId = calendarItem.Id,
IsOrganizer = true,
Email = organizerAccount.Address,
Name = organizerAccount.SenderName,
AttendenceStatus = AttendeeStatus.Accepted,
Id = Guid.NewGuid(),
IsOptionalAttendee = false,
});
}
else
{
foreach (var attendee in calendarEvent.Attendees)
{
if (attendee.Self == true)
{
// TODO:
}
else if (!string.IsNullOrEmpty(attendee.Email))
{
AttendeeStatus GetAttendenceStatus(string responseStatus)
{
return responseStatus switch
{
"accepted" => AttendeeStatus.Accepted,
"declined" => AttendeeStatus.Declined,
"tentative" => AttendeeStatus.Tentative,
"needsAction" => AttendeeStatus.NeedsAction,
_ => AttendeeStatus.NeedsAction
};
}
var eventAttendee = new CalendarEventAttendee()
{
CalendarItemId = calendarItem.Id,
IsOrganizer = attendee.Organizer ?? false,
Comment = attendee.Comment,
Email = attendee.Email,
Name = attendee.DisplayName,
AttendenceStatus = GetAttendenceStatus(attendee.ResponseStatus),
Id = Guid.NewGuid(),
IsOptionalAttendee = attendee.Optional ?? false,
};
attendees.Add(eventAttendee);
}
}
}
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
}
else
{
// We have this event already. Update it.
if (calendarEvent.Status == "cancelled")
{
// Parent event is canceled. We must delete everything.
if (string.IsNullOrEmpty(recurringEventId))
{
Log.Information("Parent event is canceled. Deleting all instances of {Id}", existingCalendarItem.Id);
await CalendarService.DeleteCalendarItemAsync(existingCalendarItem.Id).ConfigureAwait(false);
return;
}
else
{
// Child event is canceled.
// Child should live as long as parent lives, but must not be displayed to the user.
existingCalendarItem.IsHidden = true;
}
}
else
{
// Make sure to unhide the event.
// It might be marked as hidden before.
existingCalendarItem.IsHidden = false;
// Update the event properties.
}
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(existingCalendarItem);
}
private string GetOrganizerName(Event calendarEvent, MailAccount account)

View File

@@ -13,8 +13,7 @@ public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor
IMailService mailService,
IAccountService accountService,
ICalendarService calendarService,
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
{
}

View File

@@ -1,9 +1,13 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Graph.Models;
using Serilog;
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.Extensions;
using Wino.Services;
namespace Wino.Core.Integration.Processors;
@@ -13,8 +17,7 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
ICalendarService calendarService,
IMailService mailService,
IAccountService accountService,
ICalendarServiceEx calendarServiceEx,
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, calendarServiceEx, mimeFileService)
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
, IOutlookChangeProcessor
{
@@ -37,6 +40,106 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// TODO
// We parse the occurrences based on the parent event.
// There is literally no point to store them because
// type=Exception events are the exceptional childs of recurrency parent event.
if (calendarEvent.Type == EventType.Occurrence) return;
var savingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.Id);
Guid savingItemId = Guid.Empty;
if (savingItem != null)
savingItemId = savingItem.Id;
else
{
savingItemId = Guid.NewGuid();
savingItem = new CalendarItem() { Id = savingItemId };
}
DateTimeOffset eventStartDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.Start);
DateTimeOffset eventEndDateTimeOffset = OutlookIntegratorExtensions.GetDateTimeOffsetFromDateTimeTimeZone(calendarEvent.End);
var durationInSeconds = (eventEndDateTimeOffset - eventStartDateTimeOffset).TotalSeconds;
savingItem.RemoteEventId = calendarEvent.Id;
savingItem.StartDate = eventStartDateTimeOffset.DateTime;
savingItem.StartDateOffset = eventStartDateTimeOffset.Offset;
savingItem.EndDateOffset = eventEndDateTimeOffset.Offset;
savingItem.DurationInSeconds = durationInSeconds;
savingItem.Title = calendarEvent.Subject;
savingItem.Description = calendarEvent.Body?.Content;
savingItem.Location = calendarEvent.Location?.DisplayName;
if (calendarEvent.Type == EventType.Exception && !string.IsNullOrEmpty(calendarEvent.SeriesMasterId))
{
// This is a recurring event exception.
// We need to find the parent event and set it as recurring event id.
var parentEvent = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterId);
if (parentEvent != null)
{
savingItem.RecurringCalendarItemId = parentEvent.Id;
}
else
{
Log.Warning($"Parent recurring event is missing for event. Skipping creation of {calendarEvent.Id}");
return;
}
}
// Convert the recurrence pattern to string for parent recurring events.
if (calendarEvent.Type == EventType.SeriesMaster && calendarEvent.Recurrence != null)
{
savingItem.Recurrence = OutlookIntegratorExtensions.ToRfc5545RecurrenceString(calendarEvent.Recurrence);
}
savingItem.HtmlLink = calendarEvent.WebLink;
savingItem.CalendarId = assignedCalendar.Id;
savingItem.OrganizerEmail = calendarEvent.Organizer?.EmailAddress?.Address;
savingItem.OrganizerDisplayName = calendarEvent.Organizer?.EmailAddress?.Name;
savingItem.IsHidden = false;
if (calendarEvent.ResponseStatus?.Response != null)
{
switch (calendarEvent.ResponseStatus.Response.Value)
{
case ResponseType.None:
case ResponseType.NotResponded:
savingItem.Status = CalendarItemStatus.NotResponded;
break;
case ResponseType.TentativelyAccepted:
savingItem.Status = CalendarItemStatus.Tentative;
break;
case ResponseType.Accepted:
case ResponseType.Organizer:
savingItem.Status = CalendarItemStatus.Confirmed;
break;
case ResponseType.Declined:
savingItem.Status = CalendarItemStatus.Cancelled;
savingItem.IsHidden = true;
break;
default:
break;
}
}
else
{
savingItem.Status = CalendarItemStatus.Confirmed;
}
// Upsert the event.
await Connection.InsertOrReplaceAsync(savingItem);
// Manage attendees.
if (calendarEvent.Attendees != null)
{
// Clear all attendees for this event.
var attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId)).ToList();
await CalendarService.ManageEventAttendeesAsync(savingItemId, attendees).ConfigureAwait(false);
}
}
}

View File

@@ -361,207 +361,75 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
await SynchronizeCalendarsAsync(cancellationToken).ConfigureAwait(false);
bool isInitialSync = string.IsNullOrEmpty(Account.SynchronizationDeltaIdentifier);
_logger.Debug("Is initial synchronization: {IsInitialSync}", isInitialSync);
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
// TODO: Better logging and exception handling.
foreach (var calendar in localCalendars)
{
// We can do just delta sync. It will fallback to full sync if there are no sync tokens or if the token is expired.
await DeltaSynchronizeCalendarAsync(calendar).ConfigureAwait(false);
}
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
// TODO: Return proper result from delta or full sync.
return new CalendarSynchronizationResult()
{
CompletedState = SynchronizationCompletedState.Success,
DownloadedEvents = new List<ICalendarItem>(),
};
}
request.SingleEvents = false;
request.ShowDeleted = true;
private async Task FullSynchronizeCalendarAsync(AccountCalendar calendar)
{
var calendarId = calendar.RemoteCalendarId;
try
{
// Get events from the last 30 days to 1 year in the future
var timeMin = DateTime.Now.AddYears(-3);
var timeMax = DateTime.Now.AddYears(2);
var request = _calendarService.Events.List(calendarId);
request.TimeMinDateTimeOffset = timeMin;
request.TimeMaxDateTimeOffset = timeMax;
request.SingleEvents = false; // Include recurring events
request.ShowDeleted = true; // Include deleted events for synchronization
request.MaxResults = 2500; // Maximum allowed by Google Calendar API
var events = await request.ExecuteAsync();
if (events.Items != null && events.Items.Count > 0)
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
{
Console.WriteLine($"Processing {events.Items.Count} events from calendar: {calendarId}");
foreach (var googleEvent in events.Items)
{
await ProcessGoogleEventAsync(googleEvent, calendar);
}
// If a sync token is available, perform an incremental sync
request.SyncToken = calendar.SynchronizationDeltaToken;
}
else
{
Console.WriteLine($"No events found in calendar: {calendarId}");
// If no sync token, perform an initial sync
// Fetch events from the past year
request.TimeMinDateTimeOffset = DateTimeOffset.UtcNow.AddYears(-1);
}
// Store the sync token for future delta syncs
if (!string.IsNullOrEmpty(events.NextSyncToken))
string nextPageToken;
string syncToken;
var allEvents = new List<Event>();
do
{
await _gmailChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, events.NextSyncToken).ConfigureAwait(false);
// Execute the request
var events = await request.ExecuteAsync();
Console.WriteLine($"Stored sync token for calendar {calendarId} to enable delta sync");
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to synchronize calendar {calendarId}: {ex.Message}", ex);
}
}
private async Task<bool> DeltaSynchronizeCalendarAsync(AccountCalendar calendar)
{
var calendarId = calendar.RemoteCalendarId;
try
{
Console.WriteLine($"Starting delta sync for calendar: {calendarId}");
// Get the stored sync token for this calendar
var syncToken = calendar.SynchronizationDeltaToken;
if (string.IsNullOrEmpty(syncToken))
{
Console.WriteLine($"No sync token found for calendar {calendarId}. Performing full sync...");
await FullSynchronizeCalendarAsync(calendar);
return true;
}
// Create the events list request with sync token
var request = _calendarService.Events.List(calendarId);
request.SyncToken = syncToken;
request.ShowDeleted = true; // Important: include deleted events for delta sync
request.SingleEvents = false; // Include recurring events
Console.WriteLine($"Requesting delta changes with sync token: {syncToken.Substring(0, Math.Min(20, syncToken.Length))}...");
Events events;
try
{
events = await request.ExecuteAsync();
}
catch (Google.GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.Gone)
{
// Sync token has expired, need to do full sync
Console.WriteLine($"Sync token expired for calendar {calendarId}. Performing full sync...");
await FullSynchronizeCalendarAsync(calendar);
return true;
}
if (events.Items != null && events.Items.Count > 0)
{
Console.WriteLine($"Processing {events.Items.Count} delta changes for calendar: {calendarId}");
foreach (var googleEvent in events.Items)
// Process the fetched events
if (events.Items != null)
{
await ProcessDeltaCalendarEventAsync(googleEvent, calendar);
allEvents.AddRange(events.Items);
}
}
else
// Get the next page token and sync token
nextPageToken = events.NextPageToken;
syncToken = events.NextSyncToken;
// Set the next page token for subsequent requests
request.PageToken = nextPageToken;
} while (!string.IsNullOrEmpty(nextPageToken));
calendar.SynchronizationDeltaToken = syncToken;
// allEvents contains new or updated events.
// Process them and create/update local calendar items.
foreach (var @event in allEvents)
{
Console.WriteLine($"No changes found for calendar: {calendarId}");
// TODO: Exception handling for event processing.
// TODO: Also update attendees and other properties.
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
}
// Store the new sync token
if (!string.IsNullOrEmpty(events.NextSyncToken))
{
await _gmailChangeProcessor.UpdateCalendarSyncTokenAsync(calendarId, events.NextSyncToken).ConfigureAwait(false);
calendar.SynchronizationDeltaToken = events.NextSyncToken;
Console.WriteLine($"Updated sync token for calendar {calendarId}");
}
return true;
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine($"Error during delta sync for calendar {calendarId}: {ex.Message}");
return false;
}
}
/// <summary>
/// Processes a single event change from delta synchronization
/// </summary>
/// <param name="googleEvent">The Google Calendar event</param>
/// <param name="calendarId">The ID of the calendar containing the event</param>
private async Task ProcessDeltaCalendarEventAsync(Event googleEvent, AccountCalendar calendar)
{
var calendarId = calendar.RemoteCalendarId;
try
{
if (googleEvent.Status == "cancelled")
{
// Handle deleted/canceled events
await _gmailChangeProcessor.MarkEventAsDeletedAsync(googleEvent.Id, calendarId);
Console.WriteLine($"🗑️ Marked event as deleted: {googleEvent.Summary ?? googleEvent.Id}");
return;
}
// For active events (confirmed, tentative), process normally
var calendarEvent = GoogleIntegratorExtensions.MapGoogleEventToCalendarEvent(googleEvent, calendar);
var result = await _gmailChangeProcessor.UpsertEventAsync(calendarEvent);
// Sync attendees for delta events too
await SyncEventAttendeesAsync(googleEvent, calendarEvent.Id);
if (result > 0)
{
var action = await _gmailChangeProcessor.GetEventByRemoteIdAsync(googleEvent.Id) != null ? "Updated" : "Created";
Console.WriteLine($"✅ {action} event: {calendarEvent.Title} ({calendarEvent.RemoteEventId})");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Failed to process delta event {googleEvent.Id}: {ex.Message}");
}
}
/// <summary>
/// Processes a single Google Calendar event and updates the local database
/// </summary>
/// <param name="googleEvent">The Google Calendar event</param>
/// <param name="calendarId">The ID of the calendar containing the event</param>
private async Task ProcessGoogleEventAsync(Event googleEvent, AccountCalendar calendar)
{
try
{
if (googleEvent.Status == "cancelled")
{
// Handle deleted events
await _gmailChangeProcessor.DeleteEventAsync(googleEvent.Id);
Console.WriteLine($"Marked event as deleted: {googleEvent.Summary ?? googleEvent.Id}");
return;
}
var calendarEvent = googleEvent.MapGoogleEventToCalendarEvent(calendar);
await _gmailChangeProcessor.UpsertEventAsync(calendarEvent);
// Sync attendees separately
await SyncEventAttendeesAsync(googleEvent, calendarEvent.Id);
Console.WriteLine($"Processed event: {calendarEvent.Title} ({calendarEvent.RemoteEventId})");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to process event {googleEvent.Id}: {ex.Message}");
}
return default;
}
private async Task SynchronizeCalendarsAsync(CancellationToken cancellationToken = default)
@@ -643,40 +511,6 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}
}
/// <summary>
/// Syncs attendees for an event from Google Calendar data
/// </summary>
/// <param name="googleEvent">The Google Calendar event</param>
/// <param name="eventId">The internal event Guid</param>
private async Task SyncEventAttendeesAsync(Event googleEvent, Guid eventId)
{
var attendees = new List<CalendarEventAttendee>();
if (googleEvent.Attendees != null && googleEvent.Attendees.Count > 0)
{
foreach (var googleAttendee in googleEvent.Attendees)
{
var attendee = new CalendarEventAttendee
{
EventId = eventId,
Email = googleAttendee.Email ?? string.Empty,
DisplayName = googleAttendee.DisplayName,
ResponseStatus = GoogleIntegratorExtensions.FromGoogleStatus(googleAttendee.ResponseStatus),
IsOptional = googleAttendee.Optional ?? false,
IsOrganizer = googleAttendee.Organizer ?? false,
IsSelf = googleAttendee.Self ?? false,
Comment = googleAttendee.Comment,
AdditionalGuests = googleAttendee.AdditionalGuests
};
attendees.Add(attendee);
}
}
// Sync attendees (replaces existing)
await _gmailChangeProcessor.SyncAttendeesForEventAsync(eventId, attendees).ConfigureAwait(false);
}
private async Task InitializeArchiveFolderAsync()
{
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
[ObservableProperty]
private List<string> _appTerminationBehavior;
[ObservableProperty]
public partial List<string> SearchModes { get; set; }

View File

@@ -129,11 +129,11 @@ public class WinoMailCollection
private async Task HandleExistingThreadAsync(ObservableGroup<object, IMailItem> group, ThreadMailItemViewModel threadViewModel, MailCopy addedItem)
{
var existingGroupKey = GetGroupingKey(threadViewModel);
await ExecuteUIThread(() => { threadViewModel.AddMailItemViewModel(addedItem); });
var newGroupKey = GetGroupingKey(threadViewModel);
if (!existingGroupKey.Equals(newGroupKey))
{
await MoveThreadToNewGroupAsync(group, threadViewModel, newGroupKey);
@@ -294,25 +294,6 @@ public class WinoMailCollection
return null;
}
public void UpdateThumbnails(string address)
{
if (CoreDispatcher == null) return;
CoreDispatcher.ExecuteOnUIThread(() =>
{
foreach (var group in _mailItemSource)
{
foreach (var item in group)
{
if (item is MailItemViewModel mailItemViewModel && mailItemViewModel.MailCopy.FromAddress.Equals(address, StringComparison.OrdinalIgnoreCase))
{
mailItemViewModel.ThumbnailUpdatedEvent = !mailItemViewModel.ThumbnailUpdatedEvent;
}
}
}
});
}
/// <summary>
/// Fins the item container that updated mail copy belongs to and updates it.
/// </summary>

View File

@@ -1,17 +1,10 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Mail.ViewModels.Data;
public partial class AccountContactViewModel : ObservableObject
public class AccountContactViewModel : AccountContact
{
public string Address { get; set; }
public string Name { get; set; }
public string Base64ContactPicture { get; set; }
public bool IsRootContact { get; set; }
public AccountContactViewModel(AccountContact contact)
{
Address = contact.Address;
@@ -46,7 +39,4 @@ public partial class AccountContactViewModel : ObservableObject
/// Display name of the contact in a format: Name <Address>.
/// </summary>
public string DisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? Address.ToLowerInvariant() : $"{Name} <{Address.ToLowerInvariant()}>";
[ObservableProperty]
public partial bool ThumbnailUpdatedEvent { get; set; }
}

View File

@@ -13,7 +13,7 @@ namespace Wino.Mail.ViewModels.Data;
public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IMailItem
{
[ObservableProperty]
public partial MailCopy MailCopy { get; set; } = mailCopy;
private MailCopy mailCopy = mailCopy;
public Guid UniqueId => ((IMailItem)MailCopy).UniqueId;
public string ThreadId => ((IMailItem)MailCopy).ThreadId;
@@ -23,13 +23,10 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableObject, IM
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
[ObservableProperty]
public partial bool ThumbnailUpdatedEvent { get; set; } = false;
private bool isCustomFocused;
[ObservableProperty]
public partial bool IsCustomFocused { get; set; }
[ObservableProperty]
public partial bool IsSelected { get; set; }
private bool isSelected;
public bool IsFlagged
{

View File

@@ -41,8 +41,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IRecipient<AccountSynchronizationCompleted>,
IRecipient<NewMailSynchronizationRequested>,
IRecipient<AccountSynchronizerStateChanged>,
IRecipient<AccountCacheResetMessage>,
IRecipient<ThumbnailAdded>
IRecipient<AccountCacheResetMessage>
{
private bool isChangingFolder = false;
@@ -1141,6 +1140,4 @@ public partial class MailListPageViewModel : MailBaseViewModel,
});
}
}
public void Receive(ThumbnailAdded message) => MailCollection.UpdateThumbnails(message.Email);
}

View File

@@ -25,14 +25,12 @@ using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Mail.ViewModels;
public partial class MailRenderingPageViewModel : MailBaseViewModel,
IRecipient<NewMailItemRenderingRequestedEvent>,
IRecipient<ThumbnailAdded>,
ITransferProgress // For listening IMAP message download progress.
{
private readonly IMailDialogService _dialogService;
@@ -790,27 +788,4 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
Log.Error(ex, "Failed to render mail.");
}
}
public void Receive(ThumbnailAdded message)
{
UpdateThumbnails(ToItems, message.Email);
UpdateThumbnails(CcItems, message.Email);
UpdateThumbnails(BccItems, message.Email);
}
private void UpdateThumbnails(ObservableCollection<AccountContactViewModel> items, string email)
{
if (Dispatcher == null || items.Count == 0) return;
Dispatcher.ExecuteOnUIThread(() =>
{
foreach (var item in items)
{
if (item.Address.Equals(email, StringComparison.OrdinalIgnoreCase))
{
item.ThumbnailUpdatedEvent = !item.ThumbnailUpdatedEvent;
}
}
});
}
}

View File

@@ -1,19 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Mail.ViewModels;
public partial class MessageListPageViewModel : MailBaseViewModel
public class MessageListPageViewModel : MailBaseViewModel
{
public IPreferencesService PreferencesService { get; }
private readonly IThumbnailService _thumbnailService;
private int selectedMarkAsOptionIndex;
public int SelectedMarkAsOptionIndex
{
get => selectedMarkAsOptionIndex;
@@ -48,7 +46,9 @@ public partial class MessageListPageViewModel : MailBaseViewModel
];
#region Properties
private int leftHoverActionIndex;
public int LeftHoverActionIndex
{
get => leftHoverActionIndex;
@@ -61,7 +61,9 @@ public partial class MessageListPageViewModel : MailBaseViewModel
}
}
private int centerHoverActionIndex;
public int CenterHoverActionIndex
{
get => centerHoverActionIndex;
@@ -75,6 +77,7 @@ public partial class MessageListPageViewModel : MailBaseViewModel
}
private int rightHoverActionIndex;
public int RightHoverActionIndex
{
get => rightHoverActionIndex;
@@ -86,21 +89,18 @@ public partial class MessageListPageViewModel : MailBaseViewModel
}
}
}
#endregion
public MessageListPageViewModel(IPreferencesService preferencesService, IThumbnailService thumbnailService)
public MessageListPageViewModel(IMailDialogService dialogService,
IPreferencesService preferencesService)
{
PreferencesService = preferencesService;
_thumbnailService = thumbnailService;
leftHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.LeftHoverAction);
centerHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.CenterHoverAction);
rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction);
SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues<MailMarkAsOption>(), PreferencesService.MarkAsPreference);
}
[RelayCommand]
private async Task ClearAvatarsCacheAsync()
{
await _thumbnailService.ClearCache();
}
}

View File

@@ -30,7 +30,6 @@
<DataTemplate x:Key="ClickableAccountMenuTemplate" x:DataType="menu:AccountMenuItem">
<controls:AccountNavigationItem
x:Name="AccountItem"
Height="50"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
BindingData="{x:Bind}"
@@ -58,12 +57,19 @@
Value="{x:Bind UnreadItemCount, Mode=OneWay}" />
</muxc:NavigationViewItem.InfoBadge>
<Grid>
<Grid
MaxHeight="70"
Margin="0,8"
RowSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2" />
</Grid.RowDefinitions>
<StackPanel VerticalAlignment="Center">
<TextBlock
x:Name="AccountNameTextblock"
@@ -74,7 +80,8 @@
TextTrimming="CharacterEllipsis" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
FontSize="13"
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsSelected), Mode=OneWay}"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Parameter.Address, Mode=OneWay}"
@@ -83,6 +90,8 @@
<PathIcon
x:Name="AttentionIcon"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -95,6 +104,7 @@
Grid.ColumnSpan="3"
Width="10"
Height="10"
Margin="0,8,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Background="{ThemeResource AppBarItemBackgroundThemeBrush}"
@@ -159,7 +169,7 @@
<!-- Inbox or any other folders. -->
<DataTemplate x:Key="FolderMenuTemplate" x:DataType="menu:FolderMenuItem">
<coreControls:WinoNavigationViewItem
MinHeight="40"
MinHeight="30"
AllowDrop="True"
ContextRequested="MenuItemContextRequested"
DataContext="{x:Bind}"
@@ -190,7 +200,6 @@
<muxc:NavigationViewItem.Content>
<Grid
x:Name="FolderBackgroundGrid"
MaxHeight="36"
Padding="2"
VerticalAlignment="Center">
<Grid
@@ -252,7 +261,7 @@
<PathIcon Data="F1 M 8.613281 17.5 C 8.75 17.942709 8.945312 18.359375 9.199219 18.75 L 4.921875 18.75 C 4.433594 18.75 3.966471 18.650717 3.520508 18.452148 C 3.074544 18.25358 2.683919 17.986654 2.348633 17.651367 C 2.013346 17.31608 1.746419 16.925455 1.547852 16.479492 C 1.349284 16.033529 1.25 15.566406 1.25 15.078125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 L 15.078125 1.25 C 15.566406 1.25 16.033527 1.349285 16.479492 1.547852 C 16.925455 1.74642 17.31608 2.013348 17.651367 2.348633 C 17.986652 2.68392 18.25358 3.074545 18.452148 3.520508 C 18.650715 3.966473 18.75 4.433594 18.75 4.921875 L 18.75 6.572266 C 18.580729 6.344402 18.390299 6.132813 18.178711 5.9375 C 17.967121 5.742188 17.740885 5.566407 17.5 5.410156 L 17.5 4.951172 C 17.5 4.625651 17.433268 4.314779 17.299805 4.018555 C 17.16634 3.722332 16.987305 3.461914 16.762695 3.237305 C 16.538086 3.012695 16.277668 2.83366 15.981445 2.700195 C 15.685221 2.566732 15.374349 2.5 15.048828 2.5 L 4.951172 2.5 C 4.619141 2.5 4.303385 2.568359 4.003906 2.705078 C 3.704427 2.841797 3.44401 3.02409 3.222656 3.251953 C 3.001302 3.479818 2.825521 3.745117 2.695312 4.047852 C 2.565104 4.350587 2.5 4.66797 2.5 5 L 13.310547 5 C 12.60091 5.266928 11.998697 5.683594 11.503906 6.25 L 2.5 6.25 L 2.5 15.048828 C 2.5 15.38737 2.568359 15.704753 2.705078 16.000977 C 2.841797 16.297201 3.024088 16.55599 3.251953 16.777344 C 3.479818 16.998697 3.745117 17.174479 4.047852 17.304688 C 4.350586 17.434896 4.667969 17.5 5 17.5 Z" />
</coreControls:WinoNavigationViewItem.Icon>
<Grid Height="50">
<Grid MinHeight="50">
<StackPanel VerticalAlignment="Center" Spacing="0">
<TextBlock
x:Name="AccountNameTextblock"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,28 +1,30 @@
using System.Collections;
using System.Collections.Specialized;
using System.Windows.Input;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Xaml.Interactivity;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Wino.Core.Domain.Interfaces;
using Wino.Controls;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.UWP.Controls;
using Wino.Helpers;
namespace Wino.Behaviors;
public partial class BindableCommandBarBehavior : Behavior<CommandBar>
public class BindableCommandBarBehavior : Behavior<CommandBar>
{
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>();
public static readonly DependencyProperty PrimaryCommandsProperty = DependencyProperty.Register(
"PrimaryCommands", typeof(object), typeof(BindableCommandBarBehavior),
new PropertyMetadata(null, UpdateCommands));
[GeneratedDependencyProperty]
public partial ICommand ItemClickedCommand { get; set; }
public ICommand ItemClickedCommand
{
get { return (ICommand)GetValue(ItemClickedCommandProperty); }
set { SetValue(ItemClickedCommandProperty, value); }
}
public static readonly DependencyProperty ItemClickedCommandProperty = DependencyProperty.Register(nameof(ItemClickedCommand), typeof(ICommand), typeof(BindableCommandBarBehavior), new PropertyMetadata(null));
public object PrimaryCommands
{
@@ -81,7 +83,7 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
AssociatedObject.PrimaryCommands.Clear();
AssociatedObject.SecondaryCommands.Clear();
if (PrimaryCommands is not IEnumerable enumerable) return;
if (!(PrimaryCommands is IEnumerable enumerable)) return;
foreach (var command in enumerable)
{
@@ -96,26 +98,19 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
else
{
var label = XamlHelpers.GetOperationString(mailOperationMenuItem.Operation);
var labelPosition = string.IsNullOrWhiteSpace(label) || !_preferencesService.IsShowActionLabelsEnabled ?
CommandBarLabelPosition.Collapsed : CommandBarLabelPosition.Default;
menuItem = new AppBarButton
{
Width = double.NaN,
MinWidth = 40,
Icon = new WinoFontIcon() { Glyph = ControlConstants.WinoIconFontDictionary[XamlHelpers.GetWinoIconGlyph(mailOperationMenuItem.Operation)] },
Label = label,
LabelPosition = labelPosition,
LabelPosition = string.IsNullOrWhiteSpace(label) ? CommandBarLabelPosition.Collapsed : CommandBarLabelPosition.Default,
DataContext = mailOperationMenuItem,
};
if (!string.IsNullOrWhiteSpace(label))
ToolTip toolTip = new ToolTip
{
var toolTip = new ToolTip
{
Content = label
};
ToolTipService.SetToolTip((DependencyObject)menuItem, toolTip);
}
Content = label
};
ToolTipService.SetToolTip((DependencyObject)menuItem, toolTip);
((AppBarButton)menuItem).Click -= Button_Click;
((AppBarButton)menuItem).Click += Button_Click;
@@ -169,7 +164,7 @@ public partial class BindableCommandBarBehavior : Behavior<CommandBar>
private static void UpdateCommands(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if (dependencyObject is not BindableCommandBarBehavior behavior) return;
if (!(dependencyObject is BindableCommandBarBehavior behavior)) return;
if (dependencyPropertyChangedEventArgs.OldValue is INotifyCollectionChanged oldList)
{

View File

@@ -43,6 +43,10 @@ public partial class AccountNavigationItem : WinoNavigationViewItem
_itemsRepeater = GetTemplateChild(PART_NavigationViewItemMenuItemsHost) as ItemsRepeater;
_selectionIndicator = GetTemplateChild(PART_SelectionIndicator) as Windows.UI.Xaml.Shapes.Rectangle;
if (_itemsRepeater == null) return;
(_itemsRepeater.Layout as StackLayout).Spacing = 0;
UpdateSelectionBorder();
}
@@ -56,6 +60,12 @@ public partial class AccountNavigationItem : WinoNavigationViewItem
{
if (_selectionIndicator == null) return;
// Adjsuting Margin in the styles are not possible due to the fact that we use the same tempalte for different types of menu items.
// Account templates listed under merged accounts will have Padding of 44. We must adopt to that.
bool hasParentMenuItem = BindingData is IAccountMenuItem accountMenuItem && accountMenuItem.ParentMenuItem != null;
_selectionIndicator.Margin = !hasParentMenuItem ? new Thickness(-44, 12, 0, 12) : new Thickness(-60, 12, -60, 12);
_selectionIndicator.Scale = IsActiveAccount ? new Vector3(1, 1, 1) : new Vector3(0, 0, 0);
_selectionIndicator.Visibility = IsActiveAccount ? Visibility.Visible : Visibility.Collapsed;
}

View File

@@ -5,14 +5,13 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Fernandezja.ColorHashSharp;
using Microsoft.Extensions.DependencyInjection;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Shapes;
using Wino.Core.Domain.Interfaces;
using Wino.Core.UWP.Services;
namespace Wino.Controls;
@@ -22,21 +21,12 @@ public partial class ImagePreviewControl : Control
private const string PART_InitialsTextBlock = "InitialsTextBlock";
private const string PART_KnownHostImage = "KnownHostImage";
private const string PART_Ellipse = "Ellipse";
private const string PART_FaviconSquircle = "FaviconSquircle";
private const string PART_FaviconImage = "FaviconImage";
#region Dependency Properties
public static readonly DependencyProperty FromNameProperty = DependencyProperty.Register(nameof(FromName), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnInformationChanged));
public static readonly DependencyProperty FromAddressProperty = DependencyProperty.Register(nameof(FromAddress), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnInformationChanged));
public static readonly DependencyProperty SenderContactPictureProperty = DependencyProperty.Register(nameof(SenderContactPicture), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnInformationChanged)));
public static readonly DependencyProperty ThumbnailUpdatedEventProperty = DependencyProperty.Register(nameof(ThumbnailUpdatedEvent), typeof(bool), typeof(ImagePreviewControl), new PropertyMetadata(false, new PropertyChangedCallback(OnInformationChanged)));
public bool ThumbnailUpdatedEvent
{
get { return (bool)GetValue(ThumbnailUpdatedEventProperty); }
set { SetValue(ThumbnailUpdatedEventProperty, value); }
}
public static readonly DependencyProperty FromNameProperty = DependencyProperty.Register(nameof(FromName), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnAddressInformationChanged));
public static readonly DependencyProperty FromAddressProperty = DependencyProperty.Register(nameof(FromAddress), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnAddressInformationChanged));
public static readonly DependencyProperty SenderContactPictureProperty = DependencyProperty.Register(nameof(SenderContactPicture), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnAddressInformationChanged)));
/// <summary>
/// Gets or sets base64 string of the sender contact picture.
@@ -65,8 +55,6 @@ public partial class ImagePreviewControl : Control
private Grid InitialsGrid;
private TextBlock InitialsTextblock;
private Image KnownHostImage;
private Border FaviconSquircle;
private Image FaviconImage;
private CancellationTokenSource contactPictureLoadingCancellationTokenSource;
public ImagePreviewControl()
@@ -82,13 +70,11 @@ public partial class ImagePreviewControl : Control
InitialsTextblock = GetTemplateChild(PART_InitialsTextBlock) as TextBlock;
KnownHostImage = GetTemplateChild(PART_KnownHostImage) as Image;
Ellipse = GetTemplateChild(PART_Ellipse) as Ellipse;
FaviconSquircle = GetTemplateChild(PART_FaviconSquircle) as Border;
FaviconImage = GetTemplateChild(PART_FaviconImage) as Image;
UpdateInformation();
}
private static void OnInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
private static void OnAddressInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is ImagePreviewControl control)
control.UpdateInformation();
@@ -96,7 +82,7 @@ public partial class ImagePreviewControl : Control
private async void UpdateInformation()
{
if ((KnownHostImage == null && FaviconSquircle == null) || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress)))
if (KnownHostImage == null || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress)))
return;
// Cancel active image loading if exists.
@@ -105,100 +91,81 @@ public partial class ImagePreviewControl : Control
contactPictureLoadingCancellationTokenSource.Cancel();
}
string contactPicture = SenderContactPicture;
var host = ThumbnailService.GetHost(FromAddress);
var isAvatarThumbnail = false;
bool isKnownHost = false;
if (string.IsNullOrEmpty(contactPicture) && !string.IsNullOrEmpty(FromAddress))
if (!string.IsNullOrEmpty(host))
{
contactPicture = await App.Current.ThumbnailService.GetThumbnailAsync(FromAddress);
isAvatarThumbnail = true;
var tuple = ThumbnailService.CheckIsKnown(host);
isKnownHost = tuple.Item1;
host = tuple.Item2;
}
if (!string.IsNullOrEmpty(contactPicture))
if (isKnownHost)
{
if (isAvatarThumbnail && FaviconSquircle != null && FaviconImage != null)
{
// Show favicon in squircle
FaviconSquircle.Visibility = Visibility.Visible;
InitialsGrid.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
// Unrealize others.
var bitmapImage = await GetBitmapImageAsync(contactPicture);
KnownHostImage.Visibility = Visibility.Visible;
InitialsGrid.Visibility = Visibility.Collapsed;
if (bitmapImage != null)
{
FaviconImage.Source = bitmapImage;
}
}
else
// Apply company logo.
KnownHostImage.Source = new BitmapImage(new Uri(ThumbnailService.GetKnownHostImage(host)));
}
else
{
KnownHostImage.Visibility = Visibility.Collapsed;
InitialsGrid.Visibility = Visibility.Visible;
if (!string.IsNullOrEmpty(SenderContactPicture))
{
// Show normal avatar (tondo)
FaviconSquircle.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
InitialsGrid.Visibility = Visibility.Visible;
contactPictureLoadingCancellationTokenSource = new CancellationTokenSource();
try
{
var brush = await GetContactImageBrushAsync(contactPicture);
var brush = await GetContactImageBrushAsync();
if (brush != null)
if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false)
{
if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false)
{
Ellipse.Fill = brush;
InitialsTextblock.Text = string.Empty;
}
Ellipse.Fill = brush;
InitialsTextblock.Text = string.Empty;
}
}
catch (Exception)
{
// Log exception.
Debugger.Break();
}
}
}
else
{
FaviconSquircle.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
InitialsGrid.Visibility = Visibility.Visible;
else
{
var colorHash = new ColorHash();
var rgb = colorHash.Rgb(FromAddress);
var colorHash = new ColorHash();
var rgb = colorHash.Rgb(FromAddress);
Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B));
InitialsTextblock.Text = ExtractInitialsFromName(FromName);
Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B));
InitialsTextblock.Text = ExtractInitialsFromName(FromName);
}
}
}
private static async Task<ImageBrush> GetContactImageBrushAsync(string base64)
private async Task<ImageBrush> GetContactImageBrushAsync()
{
// Load the image from base64 string.
var bitmapImage = await GetBitmapImageAsync(base64);
var bitmapImage = new BitmapImage();
if (bitmapImage == null) return null;
var imageArray = Convert.FromBase64String(SenderContactPicture);
var imageStream = new MemoryStream(imageArray);
var randomAccessImageStream = imageStream.AsRandomAccessStream();
randomAccessImageStream.Seek(0);
await bitmapImage.SetSourceAsync(randomAccessImageStream);
return new ImageBrush() { ImageSource = bitmapImage };
}
private static async Task<BitmapImage> GetBitmapImageAsync(string base64)
{
try
{
var bitmapImage = new BitmapImage();
var imageArray = Convert.FromBase64String(base64);
var imageStream = new MemoryStream(imageArray);
var randomAccessImageStream = imageStream.AsRandomAccessStream();
randomAccessImageStream.Seek(0);
await bitmapImage.SetSourceAsync(randomAccessImageStream);
return bitmapImage;
}
catch (Exception) { }
return null;
}
public string ExtractInitialsFromName(string name)
{
// Change from name to from address in case of name doesn't exists.

View File

@@ -76,7 +76,6 @@
FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}"
FromName="{x:Bind MailItem.FromName, Mode=OneWay}"
SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}"
ThumbnailUpdatedEvent="{x:Bind IsThumbnailUpdated, Mode=OneWay}"
Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" />
<Grid

View File

@@ -1,7 +1,6 @@
using System.Linq;
using System.Numerics;
using System.Windows.Input;
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain;
@@ -34,13 +33,6 @@ public sealed partial class MailItemDisplayInformationControl : UserControl
public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThumbnailUpdatedProperty = DependencyProperty.Register(nameof(IsThumbnailUpdated), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public bool IsThumbnailUpdated
{
get { return (bool)GetValue(IsThumbnailUpdatedProperty); }
set { SetValue(IsThumbnailUpdatedProperty, value); }
}
public bool IsThreadExpanded
{

View File

@@ -55,8 +55,7 @@
DefaultLabelPosition="Collapsed"
IsOpen="False">
<AppBarButton
Width="Auto"
MinWidth="40"
Width="48"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="Light Theme"
@@ -67,8 +66,7 @@
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Width="48"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="Dark Theme"
@@ -81,8 +79,7 @@
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorBold, Mode=TwoWay}"
Label="Bold"
ToolTipService.ToolTip="Bold">
@@ -91,8 +88,7 @@
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorItalic, Mode=TwoWay}"
Label="Italic"
ToolTipService.ToolTip="Italic">
@@ -101,8 +97,7 @@
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorUnderline, Mode=TwoWay}"
Label="Underline"
ToolTipService.ToolTip="Underline">
@@ -111,8 +106,7 @@
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorStrikethrough, Mode=TwoWay}"
Label="Stroke"
ToolTipService.ToolTip="Stroke">
@@ -122,8 +116,7 @@
</AppBarToggleButton>
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorUl, Mode=TwoWay}"
Label="Bullet List"
ToolTipService.ToolTip="Bullet List">
@@ -132,8 +125,7 @@
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorOl, Mode=TwoWay}"
Label="Ordered List"
ToolTipService.ToolTip="Ordered List">
@@ -145,8 +137,7 @@
<AppBarSeparator />
<AppBarButton
Width="Auto"
MinWidth="40"
Width="48"
Click="{x:Bind WebViewEditor.EditorOutdentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorOutdentEnabled, Mode=OneWay}"
Label="Decrease Indent"
@@ -156,8 +147,7 @@
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Width="48"
Click="{x:Bind WebViewEditor.EditorIndentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorIndentEnabled, Mode=OneWay}"
Label="Increase Indent"
@@ -167,10 +157,7 @@
</AppBarButton.Icon>
</AppBarButton>
<AppBarElementContainer
Width="Auto"
MinWidth="40"
VerticalAlignment="Center">
<AppBarElementContainer VerticalAlignment="Center">
<ComboBox
Background="Transparent"
BorderBrush="Transparent"
@@ -214,8 +201,7 @@
</AppBarElementContainer>
<AppBarSeparator />
<AppBarButton
Width="Auto"
MinWidth="40"
Width="48"
Click="{x:Bind WebViewEditor.ShowImagePicker}"
Label="Add Image"
ToolTipService.ToolTip="{x:Bind domain:Translator.Photos}">
@@ -232,8 +218,7 @@
</AppBarButton.Content>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Width="48"
Click="{x:Bind WebViewEditor.ShowEmojiPicker}"
Label="Add Emoji"
ToolTipService.ToolTip="{x:Bind domain:Translator.Emoji}">
@@ -243,8 +228,7 @@
</AppBarButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
Width="48"
IsChecked="{x:Bind WebViewEditor.IsEditorWebViewEditor, Mode=TwoWay}"
Label="Webview ToolBar"
ToolTipService.ToolTip="Webview ToolBar">

View File

@@ -1,4 +1,5 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
@@ -10,11 +11,18 @@ namespace Wino.MenuFlyouts;
public partial class WinoOperationFlyoutItem<TOperationMenuItem> : MenuFlyoutItem, IDisposable where TOperationMenuItem : IMenuOperation
{
private const double CustomHeight = 35;
public TOperationMenuItem Operation { get; set; }
Action<TOperationMenuItem> Clicked { get; set; }
public WinoOperationFlyoutItem(TOperationMenuItem operationMenuItem, Action<TOperationMenuItem> clicked)
{
Margin = new Thickness(4, 2, 4, 2);
CornerRadius = new CornerRadius(6, 6, 6, 6);
MinHeight = CustomHeight;
Operation = operationMenuItem;
IsEnabled = operationMenuItem.IsEnabled;

View File

@@ -28,27 +28,13 @@
Foreground="White" />
</Grid>
<!-- Squircle for favicon -->
<Border
x:Name="FaviconSquircle"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent"
CornerRadius="6"
Visibility="Collapsed">
<Image x:Name="FaviconImage" Stretch="Fill" />
</Border>
<Image
x:Name="KnownHostImage"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="UniformToFill"
Visibility="Collapsed" />
Stretch="UniformToFill" />
</Grid>
</ControlTemplate>
</Setter.Value>

View File

@@ -90,7 +90,6 @@
<Rectangle
x:Name="CustomSelectionIndicator"
Width="3"
Margin="-44,12,0,12"
HorizontalAlignment="Left"
Fill="{ThemeResource NavigationViewSelectionIndicatorForeground}"
Opacity="1"
@@ -112,7 +111,7 @@
x:Load="False"
Visibility="Collapsed">
<muxc:ItemsRepeater.Layout>
<muxc:StackLayout Orientation="Vertical" Spacing="0" />
<muxc:StackLayout Orientation="Vertical" />
</muxc:ItemsRepeater.Layout>
</muxc:ItemsRepeater>

View File

@@ -10,16 +10,11 @@
mc:Ignorable="d">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<StackPanel Spacing="3">
<Image
Width="50"
Height="50"
Source="ms-appx:///Images/StoreLogo.png" />
Source="ms-appx:///Assets/StoreLogo.png" />
<TextBlock
Margin="0,6,0,4"
HorizontalAlignment="Center"
@@ -172,14 +167,9 @@
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard Header="Wino Mail">
<controls:SettingsCard.HeaderIcon>
<BitmapIcon ShowAsMonochrome="False" UriSource="ms-appx:///Images/StoreLogo.png" />
</controls:SettingsCard.HeaderIcon>
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" IsTextSelectionEnabled="True">
<Run Text="{x:Bind domain:Translator.SettingsAboutVersion}" /><Run Text="{x:Bind ViewModel.VersionName}" />
</TextBlock>
</controls:SettingsCard>
<TextBlock Margin="0,0,4,0" HorizontalAlignment="Right">
<Run Text="{x:Bind domain:Translator.SettingsAboutVersion}" /><Run Text="{x:Bind ViewModel.VersionName}" />
</TextBlock>
</StackPanel>
</ScrollViewer>
</abstract:AboutPageAbstract>

View File

@@ -70,12 +70,6 @@
<ScrollViewer>
<StackPanel Spacing="4">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<controls:SettingsCard
Command="{x:Bind ViewModel.EditAccountDetailsCommand}"
Description="{x:Bind domain:Translator.SettingsEditAccountDetails_Description}"

View File

@@ -190,8 +190,6 @@
IsDynamicOverflowEnabled="True"
OverflowButtonAlignment="Left">
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="Light Theme"
@@ -202,8 +200,6 @@
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="Dark Theme"
@@ -227,38 +223,22 @@
</toolkit:TabbedCommandBar.PaneCustomContent>
<toolkit:TabbedCommandBar.MenuItems>
<toolkit:TabbedCommandBarItem DefaultLabelPosition="Collapsed" Header="{x:Bind domain:Translator.EditorToolbarOption_Format}">
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorBold, Mode=TwoWay}"
Label="Bold">
<AppBarToggleButton IsChecked="{x:Bind WebViewEditor.IsEditorBold, Mode=TwoWay}" Label="Bold">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BoldPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorItalic, Mode=TwoWay}"
Label="Italic">
<AppBarToggleButton IsChecked="{x:Bind WebViewEditor.IsEditorItalic, Mode=TwoWay}" Label="Italic">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource ItalicPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorUnderline, Mode=TwoWay}"
Label="Underline">
<AppBarToggleButton IsChecked="{x:Bind WebViewEditor.IsEditorUnderline, Mode=TwoWay}" Label="Underline">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource UnderlinePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorStrikethrough, Mode=TwoWay}"
Label="Stroke">
<AppBarToggleButton IsChecked="{x:Bind WebViewEditor.IsEditorStrikethrough, Mode=TwoWay}" Label="Stroke">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource StrikePathIcon}" />
</AppBarToggleButton.Icon>
@@ -266,21 +246,13 @@
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorUl, Mode=TwoWay}"
Label="Bullet List">
<AppBarToggleButton IsChecked="{x:Bind WebViewEditor.IsEditorUl, Mode=TwoWay}" Label="Bullet List">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BulletedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorOl, Mode=TwoWay}"
Label="Ordered List">
<AppBarToggleButton IsChecked="{x:Bind WebViewEditor.IsEditorOl, Mode=TwoWay}" Label="Ordered List">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource OrderedListPathIcon}" />
</AppBarToggleButton.Icon>
@@ -289,8 +261,6 @@
<AppBarSeparator />
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.EditorOutdentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorOutdentEnabled, Mode=OneWay}"
Label="Outdent">
@@ -302,8 +272,6 @@
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.EditorIndentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorIndentEnabled, Mode=OneWay}"
Label="Indent">
@@ -314,11 +282,7 @@
</AppBarButton.Content>
</AppBarButton>
<AppBarElementContainer
Width="Auto"
MinWidth="40"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<AppBarElementContainer HorizontalAlignment="Center" VerticalAlignment="Center">
<ComboBox
x:Name="AlignmentListView"
VerticalAlignment="Center"
@@ -366,8 +330,6 @@
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorWebViewEditor, Mode=TwoWay}"
Label="{x:Bind domain:Translator.EditorTooltip_WebViewEditor}"
ToolTipService.ToolTip="{x:Bind domain:Translator.EditorTooltip_WebViewEditor}">

View File

@@ -52,7 +52,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
if (e.NewFocusedElement == WebViewEditor)
{
await WebViewEditor.FocusEditorAsync(true);
await WebViewEditor.FocusEditorAsync(false);
}
}

View File

@@ -113,7 +113,6 @@
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailCopy, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"
@@ -137,7 +136,6 @@
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
IsCustomFocused="{x:Bind IsCustomFocused, Mode=OneWay}"
IsHoverActionsEnabled="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsHoverActionsEnabled, Mode=OneWay}"
IsThumbnailUpdated="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}"
LeftHoverAction="{Binding ElementName=root, Path=ViewModel.PreferencesService.LeftHoverAction, Mode=OneWay}"
MailItem="{x:Bind MailCopy, Mode=OneWay}"
Prefer24HourTimeFormat="{Binding ElementName=root, Path=ViewModel.PreferencesService.Prefer24HourTimeFormat, Mode=OneWay}"

View File

@@ -46,8 +46,7 @@
Height="36"
FromAddress="{x:Bind Address}"
FromName="{x:Bind Name}"
SenderContactPicture="{x:Bind Base64ContactPicture}"
ThumbnailUpdatedEvent="{x:Bind ThumbnailUpdatedEvent, Mode=OneWay}" />
SenderContactPicture="{x:Bind Base64ContactPicture}" />
<TextBlock Grid.Column="1" Text="{x:Bind Name}" />
@@ -157,11 +156,6 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ChildrenTransitions>
<TransitionCollection>
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</Grid.ChildrenTransitions>
<Border
Background="{ThemeResource WinoContentZoneBackgroud}"

View File

@@ -51,13 +51,7 @@
</Page.Resources>
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<StackPanel Spacing="3">
<!-- Accent Color -->
<controls:SettingsExpander
Description="{x:Bind domain:Translator.SettingsAccentColor_Description}"

View File

@@ -10,7 +10,7 @@
mc:Ignorable="d">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsAppPreferences_StartupBehavior_Description}" Header="{x:Bind domain:Translator.SettingsAppPreferences_StartupBehavior_Title}">
<ToggleButton
x:Name="StartupEnabledToggleButton"

View File

@@ -15,13 +15,7 @@
mc:Ignorable="d">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<StackPanel Spacing="3">
<controls:SettingsExpander Description="{x:Bind domain:Translator.AccountDetailsPage_Description}" Header="{x:Bind domain:Translator.AccountDetailsPage_Title}">
<controls:SettingsExpander.HeaderIcon>
<SymbolIcon Symbol="Contact" />

View File

@@ -10,7 +10,7 @@
mc:Ignorable="d">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel>
<controls:SettingsCard
CommandParameter="Language"
Description="{x:Bind domain:Translator.SettingsLanguage_Description}"

File diff suppressed because one or more lines are too long

View File

@@ -11,14 +11,8 @@
mc:Ignorable="d">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{x:Bind domain:Translator.SettingsReader_Title}" />
<StackPanel Spacing="4">
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.SettingsReader_Title}" />
<controls:SettingsExpander
Description="{x:Bind domain:Translator.SettingsReaderFontFamily_Description}"
Header="{x:Bind domain:Translator.SettingsReaderFont_Title}"
@@ -58,13 +52,6 @@
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsMailRendering_ActionLabels_Description}" Header="{x:Bind domain:Translator.SettingsMailRendering_ActionLabels_Title}">
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 0 7.5 C 0 6.822917 0.169271 6.191406 0.507812 5.605469 C 0.839844 5.039062 1.289062 4.589844 1.855469 4.257812 C 2.441406 3.919271 3.072917 3.75 3.75 3.75 L 16.25 3.75 C 16.927082 3.75 17.558594 3.919271 18.144531 4.257812 C 18.710938 4.589844 19.160156 5.039062 19.492188 5.605469 C 19.830729 6.191406 20 6.822917 20 7.5 L 20 11.25 C 20 11.927084 19.830729 12.558594 19.492188 13.144531 C 19.160156 13.710938 18.710938 14.160156 18.144531 14.492188 C 17.558594 14.830729 16.927082 15 16.25 15 L 3.75 15 C 3.072917 15 2.441406 14.830729 1.855469 14.492188 C 1.289062 14.160156 0.839844 13.710938 0.507812 13.144531 C 0.169271 12.558594 0 11.927084 0 11.25 Z M 3.75 5 C 3.294271 5.000001 2.875977 5.112306 2.495117 5.336914 C 2.114258 5.561524 1.811523 5.864259 1.586914 6.245117 C 1.362305 6.625978 1.25 7.044271 1.25 7.5 L 1.25 11.25 C 1.25 11.705729 1.362305 12.124023 1.586914 12.504883 C 1.811523 12.885742 2.114258 13.188477 2.495117 13.413086 C 2.875977 13.637695 3.294271 13.75 3.75 13.75 L 16.25 13.75 C 16.705729 13.75 17.124023 13.637695 17.504883 13.413086 C 17.885742 13.188477 18.188477 12.885742 18.413086 12.504883 C 18.637695 12.124023 18.75 11.705729 18.75 11.25 L 18.75 7.5 C 18.75 7.044271 18.637695 6.625978 18.413086 6.245117 C 18.188477 5.864259 17.885742 5.561524 17.504883 5.336914 C 17.124023 5.112306 16.705729 5.000001 16.25 5 Z M 8.75 9.375 C 8.75 9.199219 8.810221 9.051107 8.930664 8.930664 C 9.051106 8.810222 9.199219 8.75 9.375 8.75 L 15 8.75 C 15.17578 8.75 15.323893 8.810222 15.444336 8.930664 C 15.564778 9.051107 15.625 9.199219 15.625 9.375 C 15.625 9.550781 15.564778 9.698894 15.444336 9.819336 C 15.323893 9.939779 15.17578 10 15 10 L 9.375 10 C 9.199219 10 9.051106 9.939779 8.930664 9.819336 C 8.810221 9.698894 8.75 9.550781 8.75 9.375 Z M 7.5 9.375 C 7.5 9.713542 7.415364 10.026042 7.246094 10.3125 C 7.076823 10.598959 6.848958 10.826823 6.5625 10.996094 C 6.276042 11.165365 5.963542 11.25 5.625 11.25 C 5.286458 11.25 4.973958 11.165365 4.6875 10.996094 C 4.401042 10.826823 4.173177 10.598959 4.003906 10.3125 C 3.834635 10.026042 3.75 9.713542 3.75 9.375 C 3.75 9.036459 3.834635 8.723959 4.003906 8.4375 C 4.173177 8.151042 4.401042 7.923178 4.6875 7.753906 C 4.973958 7.584636 5.286458 7.5 5.625 7.5 C 5.963542 7.5 6.276042 7.584636 6.5625 7.753906 C 6.848958 7.923178 7.076823 8.151042 7.246094 8.4375 C 7.415364 8.723959 7.5 9.036459 7.5 9.375 Z " />
</controls:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{x:Bind ViewModel.PreferencesService.IsShowActionLabelsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander Description="{x:Bind domain:Translator.SettingsExternalContent_Description}" Header="{x:Bind domain:Translator.SettingsExternalContent_Title}">
<controls:SettingsExpander.HeaderIcon>
<PathIcon Data="F1 M 4.023438 13.75 C 3.476562 13.75 2.958984 13.636068 2.470703 13.408203 C 1.982422 13.180339 1.554362 12.875977 1.186523 12.495117 C 0.818685 12.114258 0.528971 11.674805 0.317383 11.176758 C 0.105794 10.678711 0 10.159506 0 9.619141 C 0 9.065756 0.094401 8.535156 0.283203 8.027344 C 0.472005 7.519531 0.737305 7.068686 1.079102 6.674805 C 1.420898 6.280926 1.829427 5.961915 2.304688 5.717773 C 2.779948 5.473634 3.304036 5.338543 3.876953 5.3125 L 4.189453 5.302734 C 4.261067 4.501953 4.453125 3.776043 4.765625 3.125 C 5.078125 2.473959 5.488281 1.915691 5.996094 1.450195 C 6.503906 0.984701 7.097981 0.626629 7.77832 0.375977 C 8.458658 0.125326 9.199219 0 10 0 C 10.800781 0 11.541341 0.126953 12.22168 0.380859 C 12.902018 0.634766 13.496093 0.99284 14.003906 1.455078 C 14.511718 1.917318 14.921874 2.473959 15.234375 3.125 C 15.546874 3.776043 15.738932 4.501953 15.810547 5.302734 L 15.966797 5.302734 C 16.513672 5.302734 17.032877 5.416667 17.524414 5.644531 C 18.015949 5.872396 18.44401 6.176758 18.808594 6.557617 C 19.173176 6.938477 19.462891 7.379558 19.677734 7.880859 C 19.892578 8.382162 20 8.902995 20 9.443359 L 20 9.667969 C 20 9.700521 20 9.736328 20 9.775391 C 20 9.814453 19.996744 9.850261 19.990234 9.882812 C 19.573566 9.375 19.108072 8.932292 18.59375 8.554688 C 18.489582 8.248698 18.346354 7.972006 18.164062 7.724609 C 17.98177 7.477214 17.770182 7.267253 17.529297 7.094727 C 17.28841 6.922201 17.021484 6.788737 16.728516 6.694336 C 16.435547 6.599936 16.129557 6.552735 15.810547 6.552734 C 15.472005 6.552735 15.192057 6.446941 14.970703 6.235352 C 14.749349 6.023764 14.61263 5.755209 14.560547 5.429688 C 14.527994 5.195312 14.49056 4.967448 14.448242 4.746094 C 14.405924 4.52474 14.342447 4.300131 14.257812 4.072266 C 14.088541 3.603516 13.862304 3.193359 13.579102 2.841797 C 13.295897 2.490234 12.970377 2.195639 12.602539 1.958008 C 12.2347 1.720379 11.831055 1.542969 11.391602 1.425781 C 10.952148 1.308594 10.488281 1.25 10 1.25 C 9.511719 1.25 9.047852 1.308594 8.608398 1.425781 C 8.168945 1.542969 7.765299 1.71875 7.397461 1.953125 C 7.029622 2.1875 6.704102 2.480469 6.420898 2.832031 C 6.137695 3.183594 5.911458 3.59375 5.742188 4.0625 C 5.657552 4.290365 5.594075 4.514975 5.551758 4.736328 C 5.50944 4.957683 5.472005 5.188803 5.439453 5.429688 C 5.38737 5.813803 5.236002 6.097006 4.985352 6.279297 C 4.7347 6.46159 4.423828 6.552735 4.052734 6.552734 C 3.655599 6.552735 3.286133 6.635743 2.944336 6.801758 C 2.602539 6.967774 2.306315 7.189129 2.055664 7.46582 C 1.805013 7.742514 1.608073 8.059896 1.464844 8.417969 C 1.321615 8.776042 1.25 9.147136 1.25 9.53125 C 1.25 9.928386 1.321615 10.30599 1.464844 10.664062 C 1.608073 11.022136 1.806641 11.337891 2.060547 11.611328 C 2.314453 11.884766 2.61556 12.101237 2.963867 12.260742 C 3.312174 12.420248 3.691406 12.5 4.101562 12.5 L 7.431641 12.5 C 7.373047 12.708334 7.327474 12.916667 7.294922 13.125 C 7.26237 13.333334 7.236328 13.541667 7.216797 13.75 Z M 8.75 14.375 C 8.75 13.600261 8.898111 12.871094 9.194336 12.1875 C 9.49056 11.503906 9.892578 10.908203 10.400391 10.400391 C 10.908203 9.892578 11.503906 9.490561 12.1875 9.194336 C 12.871093 8.898112 13.60026 8.75 14.375 8.75 C 14.889322 8.75 15.385741 8.816732 15.864258 8.950195 C 16.342773 9.083659 16.790363 9.272461 17.207031 9.516602 C 17.623697 9.760742 18.004557 10.055339 18.349609 10.400391 C 18.69466 10.745443 18.989258 11.126303 19.233398 11.542969 C 19.477539 11.959636 19.66634 12.407227 19.799805 12.885742 C 19.933268 13.364258 20 13.860678 20 14.375 C 20 15.14974 19.851887 15.878906 19.555664 16.5625 C 19.259439 17.246094 18.857422 17.841797 18.349609 18.349609 C 17.841797 18.857422 17.246094 19.259439 16.5625 19.555664 C 15.878906 19.851889 15.149739 20 14.375 20 C 13.59375 20 12.861328 19.853516 12.177734 19.560547 C 11.494141 19.267578 10.898438 18.867188 10.390625 18.359375 C 9.882812 17.851562 9.482422 17.255859 9.189453 16.572266 C 8.896484 15.888672 8.75 15.15625 8.75 14.375 Z M 14.375 17.578125 C 14.570312 17.578125 14.736328 17.509766 14.873047 17.373047 L 17.373047 14.873047 C 17.509766 14.736328 17.578125 14.570312 17.578125 14.375 C 17.578125 14.179688 17.509766 14.013672 17.373047 13.876953 C 17.236328 13.740234 17.070312 13.671875 16.875 13.671875 C 16.679688 13.671875 16.513672 13.740234 16.376953 13.876953 L 15 15.253906 L 15 11.875 C 14.999999 11.705729 14.93815 11.559245 14.814453 11.435547 C 14.690755 11.31185 14.544271 11.25 14.375 11.25 C 14.205729 11.25 14.059244 11.31185 13.935547 11.435547 C 13.811849 11.559245 13.75 11.705729 13.75 11.875 L 13.75 15.253906 L 12.373047 13.876953 C 12.236328 13.740234 12.070312 13.671875 11.875 13.671875 C 11.679688 13.671875 11.513672 13.740234 11.376953 13.876953 C 11.240234 14.013672 11.171875 14.179688 11.171875 14.375 C 11.171875 14.570312 11.240234 14.736328 11.376953 14.873047 L 13.876953 17.373047 C 14.013672 17.509766 14.179688 17.578125 14.375 17.578125 Z " />
@@ -84,14 +71,17 @@
</controls:SettingsCard>
<controls:SettingsCard Header="{x:Bind domain:Translator.SettingsLoadPlaintextLinks_Title}">
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="{StaticResource AddLinkPathIcon}" />
<PathIcon Data="{StaticResource AddLinkPathIcon}" />
</controls:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{x:Bind ViewModel.PreferencesService.RenderPlaintextLinks, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{x:Bind domain:Translator.SettingsComposer_Title}" />
<TextBlock
Margin="0,20,0,0"
FontWeight="SemiBold"
Text="{x:Bind domain:Translator.SettingsComposer_Title}" />
<controls:SettingsExpander
Description="{x:Bind domain:Translator.SettingsComposerFontFamily_Description}"
Header="{x:Bind domain:Translator.SettingsComposerFont_Title}"
@@ -132,4 +122,4 @@
</controls:SettingsExpander>
</StackPanel>
</ScrollViewer>
</abstract:ReadComposePanePageAbstract>
</abstract:ReadComposePanePageAbstract>

View File

@@ -53,7 +53,7 @@
</Page.Resources>
<ScrollViewer>
<Grid>
<Grid RowSpacing="41">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@@ -69,8 +69,8 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.IsSignatureEnabled, Mode=TwoWay}" />
</controls1:SettingsCard>
<StackPanel Grid.Row="1" Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{x:Bind domain:Translator.SettingsSignature_SignatureDefaults}" />
<StackPanel Grid.Row="1" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.SettingsSignature_SignatureDefaults}" />
<controls1:SettingsCard
Header="{x:Bind domain:Translator.SettingsSignature_ForNewMessages_Title}"
IsActionIconVisible="False"
@@ -100,8 +100,8 @@
</controls1:SettingsCard>
</StackPanel>
<StackPanel Grid.Row="2" Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{x:Bind domain:Translator.SettingsSignature_Signatures}" />
<StackPanel Grid.Row="2" Spacing="3">
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.SettingsSignature_Signatures}" />
<controls1:SettingsCard
Header="{x:Bind domain:Translator.SettingsSignature_AddCustomSignature_Title}"
IsActionIconVisible="False"

View File

@@ -26,13 +26,19 @@
</ItemGroup>
<ItemGroup>
<None Remove="Assets\EML\eml.png" />
<None Remove="Assets\NotificationIcons\archive.png" />
<None Remove="Assets\NotificationIcons\delete.png" />
<None Remove="Assets\NotificationIcons\dismiss.png" />
<None Remove="Assets\NotificationIcons\markread.png" />
<None Remove="Assets\NotificationIcons\profile-dark.png" />
<None Remove="Assets\NotificationIcons\profile-light.png" />
<None Remove="Assets\ReleaseNotes\1102.md" />
<None Remove="Assets\Thumbnails\airbnb.com.png" />
<None Remove="Assets\Thumbnails\apple.com.png" />
<None Remove="Assets\Thumbnails\google.com.png" />
<None Remove="Assets\Thumbnails\microsoft.com.png" />
<None Remove="Assets\Thumbnails\steampowered.com.png" />
<None Remove="Assets\Thumbnails\uber.com.png" />
<None Remove="Assets\Thumbnails\youtube.com.png" />
<None Remove="JS\editor.html" />
<None Remove="JS\editor.js" />
<None Remove="JS\global.css" />
@@ -45,13 +51,19 @@
</ItemGroup>
<ItemGroup>
<Content Include="Assets\EML\eml.png" />
<Content Include="Assets\NotificationIcons\archive.png" />
<Content Include="Assets\NotificationIcons\delete.png" />
<Content Include="Assets\NotificationIcons\dismiss.png" />
<Content Include="Assets\NotificationIcons\markread.png" />
<Content Include="Assets\NotificationIcons\profile-dark.png" />
<Content Include="Assets\NotificationIcons\profile-light.png" />
<Content Include="Assets\ReleaseNotes\1102.md" />
<Content Include="Assets\Thumbnails\airbnb.com.png" />
<Content Include="Assets\Thumbnails\apple.com.png" />
<Content Include="Assets\Thumbnails\google.com.png" />
<Content Include="Assets\Thumbnails\microsoft.com.png" />
<Content Include="Assets\Thumbnails\steampowered.com.png" />
<Content Include="Assets\Thumbnails\uber.com.png" />
<Content Include="Assets\Thumbnails\youtube.com.png" />
<Content Include="JS\editor.html" />
<Content Include="JS\editor.js" />
<Content Include="JS\global.css" />

View File

@@ -1,4 +0,0 @@
using Wino.Core.Domain.Interfaces;
namespace Wino.Messaging.UI;
public record ThumbnailAdded(string Email): IUIMessage;

View File

@@ -79,7 +79,6 @@ public partial class App : Application
services.AddTransient<INotificationBuilder, NotificationBuilder>();
services.AddTransient<IUnderlyingThemeService, UnderlyingThemeService>();
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
services.AddSingleton<IThumbnailService, ThumbnailService>();
// Register server message handler factory.
var serverMessageHandlerFactory = new ServerMessageHandlerFactory();

View File

@@ -34,12 +34,11 @@
</Resource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="gravatar-dotnet" />
<PackageReference Include="H.NotifyIcon.Wpf" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="H.NotifyIcon.Wpf" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="System.Text.Encoding.CodePages" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />

View File

@@ -1,9 +1,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using SqlKata;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Messaging.Client.Calendar;
@@ -51,7 +58,27 @@ public class CalendarService : BaseDatabaseService, ICalendarService
public async Task DeleteCalendarItemAsync(Guid calendarItemId)
{
var calendarItem = await Connection.GetAsync<CalendarItem>(calendarItemId);
if (calendarItem == null) return;
List<CalendarItem> eventsToRemove = new() { calendarItem };
// In case of parent event, delete all child events as well.
if (!string.IsNullOrEmpty(calendarItem.Recurrence))
{
var recurringEvents = await Connection.Table<CalendarItem>().Where(a => a.RecurringCalendarItemId == calendarItemId).ToListAsync().ConfigureAwait(false);
eventsToRemove.AddRange(recurringEvents);
}
foreach (var @event in eventsToRemove)
{
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == @event.Id).ConfigureAwait(false);
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == @event.Id).ConfigureAwait(false);
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(@event));
}
}
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
@@ -71,8 +98,83 @@ public class CalendarService : BaseDatabaseService, ICalendarService
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel)
{
// TODO
return new List<CalendarItem>();
// TODO: We might need to implement caching here.
// I don't know how much of the events we'll have in total, but this logic scans all events every time for given calendar.
var accountEvents = await Connection.Table<CalendarItem>()
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden).ToListAsync();
var result = new List<CalendarItem>();
foreach (var ev in accountEvents)
{
ev.AssignedCalendar = calendar;
// Parse recurrence rules
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(ev.StartDate),
End = new CalDateTime(ev.EndDate),
};
if (string.IsNullOrEmpty(ev.Recurrence))
{
// No recurrence, only check if we fall into the given period.
if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
{
result.Add(ev);
}
}
else
{
// This event has recurrences.
// Wino stores exceptional recurrent events as a separate calendar item, without the recurrence rule.
// Because each instance of recurrent event can have different attendees, properties etc.
// Even though the event is recurrent, each updated instance is a separate calendar item.
// Calculate the all recurrences, and remove the exceptional instances like hidden ones.
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
// Calculate occurrences in the range.
var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
// Get all recurrent exceptional calendar events.
var exceptionalRecurrences = await Connection.Table<CalendarItem>()
.Where(a => a.RecurringCalendarItemId == ev.Id)
.ToListAsync()
.ConfigureAwait(false);
foreach (var occurrence in occurrences)
{
var exactInstanceCheck = exceptionalRecurrences.FirstOrDefault(a =>
a.Period.OverlapsWith(dayRangeRenderModel.Period));
if (exactInstanceCheck == null)
{
// There is no exception for the period.
// Change the instance StartDate and Duration.
var recurrence = ev.CreateRecurrence(occurrence.Period.StartTime.Value, occurrence.Period.Duration.TotalSeconds);
result.Add(recurrence);
}
else
{
// There is a single instance of this recurrent event.
// It will be added as single item if it's not hidden.
// We don't need to do anything here.
}
}
}
}
return result;
}
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
@@ -119,7 +221,7 @@ public class CalendarService : BaseDatabaseService, ICalendarService
}
public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId)
=> Connection.Table<CalendarEventAttendee>().Where(x => x.EventId == calendarEventTrackingId).ToListAsync();
=> Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync();
public async Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees)
{
@@ -128,7 +230,7 @@ public class CalendarService : BaseDatabaseService, ICalendarService
// Clear all attendees.
var query = new Query()
.From(nameof(CalendarEventAttendee))
.Where(nameof(CalendarEventAttendee.EventId), calendarItemId)
.Where(nameof(CalendarEventAttendee.CalendarItemId), calendarItemId)
.AsDelete();
connection.Execute(query.GetRawQuery());
@@ -137,12 +239,67 @@ public class CalendarService : BaseDatabaseService, ICalendarService
connection.InsertAll(allAttendees);
});
return await Connection.Table<CalendarEventAttendee>().Where(a => a.EventId == calendarItemId).ToListAsync();
return await Connection.Table<CalendarEventAttendee>().Where(a => a.CalendarItemId == calendarItemId).ToListAsync();
}
public async Task<CalendarItem> GetCalendarItemTargetAsync(CalendarItemTarget targetDetails)
{
// TODO
return null;
var eventId = targetDetails.Item.Id;
// Get the event by Id first.
var item = await GetCalendarItemAsync(eventId).ConfigureAwait(false);
bool isRecurringChild = targetDetails.Item.IsRecurringChild;
bool isRecurringParent = targetDetails.Item.IsRecurringParent;
if (targetDetails.TargetType == CalendarEventTargetType.Single)
{
if (isRecurringChild)
{
if (item == null)
{
// This is an occurrence of a recurring event.
// They don't exist in db.
return targetDetails.Item;
}
else
{
// Single exception occurrence of recurring event.
// Return the item.
return item;
}
}
else if (isRecurringParent)
{
// Parent recurring events are never listed.
Debugger.Break();
return null;
}
else
{
// Single event.
return item;
}
}
else
{
// Series.
if (isRecurringChild)
{
// Return the parent.
return await GetCalendarItemAsync(targetDetails.Item.RecurringCalendarItemId.Value).ConfigureAwait(false);
}
else if (isRecurringParent)
return item;
else
{
// NA. Single events don't have series.
Debugger.Break();
return null;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -57,8 +57,7 @@ public class DatabaseService : IDatabaseService
typeof(AccountCalendar),
typeof(CalendarEventAttendee),
typeof(CalendarItem),
typeof(Reminder),
typeof(Thumbnail)
typeof(Reminder)
);
}
}

View File

@@ -17,7 +17,6 @@ public static class ServicesContainerSetup
services.AddSingleton<IMimeFileService, MimeFileService>();
services.AddTransient<ICalendarService, CalendarService>();
services.AddTransient<ICalendarServiceEx, CalendarServiceEx>();
services.AddTransient<IMailService, MailService>();
services.AddTransient<IFolderService, FolderService>();
services.AddTransient<IAccountService, AccountService>();
@@ -30,5 +29,7 @@ public static class ServicesContainerSetup
services.AddTransient<IOutlookThreadingStrategy, OutlookThreadingStrategy>();
services.AddTransient<IGmailThreadingStrategy, GmailThreadingStrategy>();
services.AddTransient<IImapThreadingStrategy, ImapThreadingStrategy>();
}
}

289
Wino.sln Normal file
View File

@@ -0,0 +1,289 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Mail", "Wino.Mail\Wino.Mail.csproj", "{68A432B8-C1B7-494C-8D6D-230788EA683E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core", "Wino.Core\Wino.Core.csproj", "{E6B1632A-8901-41E8-9DDF-6793C7698B0B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.UWP", "Wino.Core.UWP\Wino.Core.UWP.csproj", "{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.Domain", "Wino.Core.Domain\Wino.Core.Domain.csproj", "{CF3312E5-5DA0-4867-9945-49EA7598AF1F}"
ProjectSection(ProjectDependencies) = postProject
{D4919A19-E70F-4916-83D2-5D5F87BEB949} = {D4919A19-E70F-4916-83D2-5D5F87BEB949}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.BackgroundTasks", "Wino.BackgroundTasks\Wino.BackgroundTasks.csproj", "{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Mail.ViewModels", "Wino.Mail.ViewModels\Wino.Mail.ViewModels.csproj", "{D62F1C03-DA57-4709-A640-0283296A8E66}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Messaging", "Wino.Messages\Wino.Messaging.csproj", "{0C307D7E-256F-448C-8265-5622A812FBCC}"
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Wino.Packaging", "Wino.Packaging\Wino.Packaging.wapproj", "{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Server", "Wino.Server\Wino.Server.csproj", "{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.SourceGenerators", "Wino.SourceGenerators\Wino.SourceGenerators.csproj", "{D4919A19-E70F-4916-83D2-5D5F87BEB949}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.ViewModels", "Wino.Core.ViewModels\Wino.Core.ViewModels.csproj", "{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Authentication", "Wino.Authentication\Wino.Authentication.csproj", "{A4DBA01A-F315-49E0-8428-BB99D32B20F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar", "Wino.Calendar\Wino.Calendar.csproj", "{600F4979-DB7E-409D-B7DA-B60BE4C55C35}"
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Wino.Calendar.Packaging", "Wino.Calendar.Packaging\Wino.Calendar.Packaging.wapproj", "{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Services", "Wino.Services\Wino.Services.csproj", "{4000A374-59FE-4400-ACF6-D40473BECD73}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|ARM64.ActiveCfg = Debug|ARM64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|ARM64.Build.0 = Debug|ARM64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|ARM64.Deploy.0 = Debug|ARM64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|x64.ActiveCfg = Debug|x64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|x64.Build.0 = Debug|x64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|x64.Deploy.0 = Debug|x64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|x86.ActiveCfg = Debug|x86
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|x86.Build.0 = Debug|x86
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Debug|x86.Deploy.0 = Debug|x86
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|ARM64.ActiveCfg = Release|ARM64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|ARM64.Build.0 = Release|ARM64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|ARM64.Deploy.0 = Release|ARM64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|x64.ActiveCfg = Release|x64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|x64.Build.0 = Release|x64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|x64.Deploy.0 = Release|x64
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|x86.ActiveCfg = Release|x86
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|x86.Build.0 = Release|x86
{68A432B8-C1B7-494C-8D6D-230788EA683E}.Release|x86.Deploy.0 = Release|x86
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Debug|ARM64.Build.0 = Debug|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Debug|x64.ActiveCfg = Debug|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Debug|x64.Build.0 = Debug|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Debug|x86.ActiveCfg = Debug|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Debug|x86.Build.0 = Debug|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Release|ARM64.ActiveCfg = Release|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Release|ARM64.Build.0 = Release|Any CPU
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Release|x64.ActiveCfg = Release|x64
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Release|x64.Build.0 = Release|x64
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Release|x86.ActiveCfg = Release|x86
{E6B1632A-8901-41E8-9DDF-6793C7698B0B}.Release|x86.Build.0 = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.Build.0 = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.ActiveCfg = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.Build.0 = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.ActiveCfg = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.Build.0 = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.ActiveCfg = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.Build.0 = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.ActiveCfg = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.Build.0 = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.ActiveCfg = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.Build.0 = Release|x86
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Debug|ARM64.Build.0 = Debug|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Debug|x64.ActiveCfg = Debug|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Debug|x64.Build.0 = Debug|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Debug|x86.ActiveCfg = Debug|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Debug|x86.Build.0 = Debug|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Release|ARM64.ActiveCfg = Release|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Release|ARM64.Build.0 = Release|Any CPU
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Release|x64.ActiveCfg = Release|x64
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Release|x64.Build.0 = Release|x64
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Release|x86.ActiveCfg = Release|x86
{CF3312E5-5DA0-4867-9945-49EA7598AF1F}.Release|x86.Build.0 = Release|x86
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Debug|ARM64.ActiveCfg = Debug|ARM64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Debug|ARM64.Build.0 = Debug|ARM64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Debug|x64.ActiveCfg = Debug|x64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Debug|x64.Build.0 = Debug|x64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Debug|x86.ActiveCfg = Debug|x86
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Debug|x86.Build.0 = Debug|x86
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Release|ARM64.ActiveCfg = Release|ARM64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Release|ARM64.Build.0 = Release|ARM64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Release|x64.ActiveCfg = Release|x64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Release|x64.Build.0 = Release|x64
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Release|x86.ActiveCfg = Release|x86
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}.Release|x86.Build.0 = Release|x86
{D62F1C03-DA57-4709-A640-0283296A8E66}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Debug|ARM64.Build.0 = Debug|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Debug|x64.ActiveCfg = Debug|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Debug|x64.Build.0 = Debug|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Debug|x86.ActiveCfg = Debug|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Debug|x86.Build.0 = Debug|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Release|ARM64.ActiveCfg = Release|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Release|ARM64.Build.0 = Release|Any CPU
{D62F1C03-DA57-4709-A640-0283296A8E66}.Release|x64.ActiveCfg = Release|x64
{D62F1C03-DA57-4709-A640-0283296A8E66}.Release|x64.Build.0 = Release|x64
{D62F1C03-DA57-4709-A640-0283296A8E66}.Release|x86.ActiveCfg = Release|x86
{D62F1C03-DA57-4709-A640-0283296A8E66}.Release|x86.Build.0 = Release|x86
{0C307D7E-256F-448C-8265-5622A812FBCC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Debug|ARM64.Build.0 = Debug|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Debug|x64.ActiveCfg = Debug|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Debug|x64.Build.0 = Debug|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Debug|x86.ActiveCfg = Debug|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Debug|x86.Build.0 = Debug|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Release|ARM64.ActiveCfg = Release|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Release|ARM64.Build.0 = Release|Any CPU
{0C307D7E-256F-448C-8265-5622A812FBCC}.Release|x64.ActiveCfg = Release|x64
{0C307D7E-256F-448C-8265-5622A812FBCC}.Release|x64.Build.0 = Release|x64
{0C307D7E-256F-448C-8265-5622A812FBCC}.Release|x86.ActiveCfg = Release|x86
{0C307D7E-256F-448C-8265-5622A812FBCC}.Release|x86.Build.0 = Release|x86
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|ARM64.ActiveCfg = Debug|ARM64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|ARM64.Build.0 = Debug|ARM64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|ARM64.Deploy.0 = Debug|ARM64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|x64.ActiveCfg = Debug|x64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|x64.Build.0 = Debug|x64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|x64.Deploy.0 = Debug|x64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|x86.ActiveCfg = Debug|x86
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|x86.Build.0 = Debug|x86
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Debug|x86.Deploy.0 = Debug|x86
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|ARM64.ActiveCfg = Release|ARM64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|ARM64.Build.0 = Release|ARM64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|ARM64.Deploy.0 = Release|ARM64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|x64.ActiveCfg = Release|x64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|x64.Build.0 = Release|x64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|x64.Deploy.0 = Release|x64
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|x86.ActiveCfg = Release|x86
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|x86.Build.0 = Release|x86
{760F5F31-8EE3-4B83-80F3-0E4FFBCC737C}.Release|x86.Deploy.0 = Release|x86
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Debug|ARM64.ActiveCfg = Debug|ARM64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Debug|ARM64.Build.0 = Debug|ARM64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Debug|x64.ActiveCfg = Debug|x64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Debug|x64.Build.0 = Debug|x64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Debug|x86.ActiveCfg = Debug|x86
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Debug|x86.Build.0 = Debug|x86
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|ARM64.ActiveCfg = Release|ARM64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|ARM64.Build.0 = Release|ARM64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x64.ActiveCfg = Release|x64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x64.Build.0 = Release|x64
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x86.ActiveCfg = Release|x86
{3D1942E5-1A3B-4062-B4BB-156A40DA47FE}.Release|x86.Build.0 = Release|x86
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|ARM64.Build.0 = Debug|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x64.ActiveCfg = Debug|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x64.Build.0 = Debug|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x86.ActiveCfg = Debug|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Debug|x86.Build.0 = Debug|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|ARM64.ActiveCfg = Release|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|ARM64.Build.0 = Release|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x64.ActiveCfg = Release|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x64.Build.0 = Release|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x86.ActiveCfg = Release|Any CPU
{D4919A19-E70F-4916-83D2-5D5F87BEB949}.Release|x86.Build.0 = Release|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Debug|ARM64.Build.0 = Debug|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Debug|x64.ActiveCfg = Debug|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Debug|x64.Build.0 = Debug|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Debug|x86.ActiveCfg = Debug|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Debug|x86.Build.0 = Debug|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Release|ARM64.ActiveCfg = Release|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Release|ARM64.Build.0 = Release|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Release|x64.ActiveCfg = Release|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Release|x64.Build.0 = Release|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Release|x86.ActiveCfg = Release|Any CPU
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8}.Release|x86.Build.0 = Release|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Debug|ARM64.Build.0 = Debug|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Debug|x64.ActiveCfg = Debug|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Debug|x64.Build.0 = Debug|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Debug|x86.ActiveCfg = Debug|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Debug|x86.Build.0 = Debug|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|ARM64.ActiveCfg = Release|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|ARM64.Build.0 = Release|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x64.ActiveCfg = Release|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x64.Build.0 = Release|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.ActiveCfg = Release|Any CPU
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.Build.0 = Release|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM64.Build.0 = Debug|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x64.Build.0 = Debug|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x86.ActiveCfg = Debug|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x86.Build.0 = Debug|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM64.ActiveCfg = Release|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM64.Build.0 = Release|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x64.ActiveCfg = Release|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x64.Build.0 = Release|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x86.ActiveCfg = Release|Any CPU
{A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x86.Build.0 = Release|Any CPU
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.ActiveCfg = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Build.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Deploy.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.ActiveCfg = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Build.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Deploy.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.ActiveCfg = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Build.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Deploy.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.ActiveCfg = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Deploy.0 = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Build.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Deploy.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.ActiveCfg = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Build.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Deploy.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.ActiveCfg = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Build.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Deploy.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.ActiveCfg = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Build.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Deploy.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.ActiveCfg = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Build.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Deploy.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.ActiveCfg = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Build.0 = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Deploy.0 = Release|x86
{4000A374-59FE-4400-ACF6-D40473BECD73}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Debug|ARM64.Build.0 = Debug|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Debug|x64.ActiveCfg = Debug|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Debug|x64.Build.0 = Debug|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Debug|x86.ActiveCfg = Debug|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Debug|x86.Build.0 = Debug|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Release|ARM64.ActiveCfg = Release|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Release|ARM64.Build.0 = Release|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Release|x64.ActiveCfg = Release|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Release|x64.Build.0 = Release|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Release|x86.ActiveCfg = Release|Any CPU
{4000A374-59FE-4400-ACF6-D40473BECD73}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E6B1632A-8901-41E8-9DDF-6793C7698B0B} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{CF3312E5-5DA0-4867-9945-49EA7598AF1F} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{D62F1C03-DA57-4709-A640-0283296A8E66} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{0C307D7E-256F-448C-8265-5622A812FBCC} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{D4919A19-E70F-4916-83D2-5D5F87BEB949} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{53723AE8-7E7E-4D54-ADAB-0A6033255CC8} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{039AFFA8-C1CC-4E3B-8A31-6814D7557F74} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{A4DBA01A-F315-49E0-8428-BB99D32B20F9} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
{4000A374-59FE-4400-ACF6-D40473BECD73} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {721F946E-F69F-4987-823A-D084B436FC1E}
EndGlobalSection
EndGlobal