Separation of core library from the UWP app.

This commit is contained in:
Burak Kaan Köse
2024-11-30 23:05:07 +01:00
parent 4e25dbf5e3
commit 0cd1568c64
88 changed files with 481 additions and 353 deletions

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace Wino.Core.Domain.Collections
{
/// <summary>
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObservableRangeCollection<T> : ObservableCollection<T>
{
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
/// </summary>
public ObservableRangeCollection()
: base()
{
}
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">collection: The collection from which the elements are copied.</param>
/// <exception cref="ArgumentNullException">The collection parameter cannot be null.</exception>
public ObservableRangeCollection(IEnumerable<T> collection)
: base(collection)
{
}
/// <summary>
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
/// </summary>
public void AddRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Add)
{
if (notificationMode != NotifyCollectionChangedAction.Add && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Add or Reset for AddRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var startIndex = Count;
var itemsAdded = AddArrangeCore(collection);
if (!itemsAdded)
return;
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = collection is List<T> ? (List<T>)collection : new List<T>(collection);
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Add,
changedItems: changedItems,
startingIndex: startIndex);
}
/// <summary>
/// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). NOTE: with notificationMode = Remove, removed items starting index is not set because items are not guaranteed to be consecutive.
/// </summary>
public void RemoveRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Reset)
{
if (notificationMode != NotifyCollectionChangedAction.Remove && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Remove or Reset for RemoveRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
var raiseEvents = false;
foreach (var item in collection)
{
Items.Remove(item);
raiseEvents = true;
}
if (raiseEvents)
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = new List<T>(collection);
for (var i = 0; i < changedItems.Count; i++)
{
if (!Items.Remove(changedItems[i]))
{
changedItems.RemoveAt(i); //Can't use a foreach because changedItems is intended to be (carefully) modified
i--;
}
}
if (changedItems.Count == 0)
return;
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Remove,
changedItems: changedItems);
}
/// <summary>
/// Clears the current collection and replaces it with the specified item.
/// </summary>
public void Replace(T item) => ReplaceRange(new T[] { item });
/// <summary>
/// Clears the current collection and replaces it with the specified collection.
/// </summary>
public void ReplaceRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var previouslyEmpty = Items.Count == 0;
Items.Clear();
AddArrangeCore(collection);
var currentlyEmpty = Items.Count == 0;
if (previouslyEmpty && currentlyEmpty)
return;
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
}
public void InsertRange(IEnumerable<T> items)
{
CheckReentrancy();
foreach (var item in items)
Items.Insert(0, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private bool AddArrangeCore(IEnumerable<T> collection)
{
var itemAdded = false;
foreach (var item in collection)
{
Items.Add(item);
itemAdded = true;
}
return itemAdded;
}
private void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List<T> changedItems = null, int startingIndex = -1)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
if (changedItems is null)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action));
else
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex));
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using MimeKit;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Interfaces
{
public interface IContactService
{
Task<List<AccountContact>> GetAddressInformationAsync(string queryText);
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
Task SaveAddressInformationAsync(MimeMessage message);
}
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.Interfaces
{
public interface IGmailThreadingStrategy : IThreadingStrategy { }
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.Interfaces
{
public interface IImapThreadingStrategy : IThreadingStrategy { }
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MimeKit;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Reader;
namespace Wino.Core.Domain.Interfaces
{
public interface IMimeFileService
{
/// <summary>
/// Finds the EML file for the given mail id for address, parses and returns MimeMessage.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Mime message information</returns>
Task<MimeMessageInformation> GetMimeMessageInformationAsync(Guid fileId, Guid accountId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the mime message information for the given EML file bytes.
/// This override is used when EML file association launch is used
/// because we may not have the access to the file path.
/// </summary>
/// <param name="fileBytes">Byte array of the file.</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Mime message information</returns>
Task<MimeMessageInformation> GetMimeMessageInformationAsync(byte[] fileBytes, string emlFilePath, CancellationToken cancellationToken = default);
/// <summary>
/// Saves EML file to the disk.
/// </summary>
/// <param name="copy">MailCopy of the native message.</param>
/// <param name="mimeMessage">MimeMessage that is parsed from native message.</param>
/// <param name="accountId">Which account Id to save this file for.</param>
Task<bool> SaveMimeMessageAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
/// <summary>
/// Returns a path that all Mime resources (including eml) is stored for this MailCopyId
/// This is useful for storing previously rendered attachments as well.
/// </summary>
/// <param name="accountAddress">Account address</param>
/// <param name="mailCopyId">Resource mail copy id</param>
Task<string> GetMimeResourcePathAsync(Guid accountId, Guid fileId);
/// <summary>
/// Returns whether mime file exists locally or not.
/// </summary>
Task<bool> IsMimeExistAsync(Guid accountId, Guid fileId);
/// <summary>
/// Creates HtmlPreviewVisitor for the given MimeMessage.
/// </summary>
/// <param name="message">Mime</param>
/// <param name="mimeLocalPath">File path that mime is located to load resources.</param>
HtmlPreviewVisitor CreateHTMLPreviewVisitor(MimeMessage message, string mimeLocalPath);
/// <summary>
/// Deletes the given mime file from the disk.
/// </summary>
Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId);
/// <summary>
/// Prepares the final model containing rendering details.
/// </summary>
/// <param name="message">Message to render.</param>
/// <param name="mimeLocalPath">File path that physical MimeMessage is located.</param>
/// <param name="options">Rendering options</param>
MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null);
}
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.Interfaces
{
public interface IOutlookThreadingStrategy : IThreadingStrategy { }
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.MenuItems
{
public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IMailItemFolder, FolderMenuItem>>, IAccountMenuItem
{
[ObservableProperty]
private int unreadItemCount;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible))]
private double synchronizationProgress;
[ObservableProperty]
private bool _isEnabled = true;
public bool IsAttentionRequired => AttentionReason != AccountAttentionReason.None;
public bool IsSynchronizationProgressVisible => SynchronizationProgress > 0 && SynchronizationProgress < 100;
// We can't determine the progress for gmail synchronization since it is based on history changes.
public bool IsProgressIndeterminate => Parameter?.ProviderType == MailProviderType.Gmail;
public Guid AccountId => Parameter.Id;
private AccountAttentionReason attentionReason;
public AccountAttentionReason AttentionReason
{
get => attentionReason;
set
{
if (SetProperty(ref attentionReason, value))
{
OnPropertyChanged(nameof(IsAttentionRequired));
UpdateFixAccountIssueMenuItem();
}
}
}
public string AccountName
{
get => Parameter.Name;
set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n);
}
public string Base64ProfilePicture
{
get => Parameter.Name;
set => SetProperty(Parameter.Base64ProfilePictureData, value, Parameter, (u, n) => u.Base64ProfilePictureData = n);
}
public IEnumerable<MailAccount> HoldingAccounts => new List<MailAccount> { Parameter };
public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent)
{
UpdateAccount(account);
}
public void UpdateAccount(MailAccount account)
{
Parameter = account;
AccountName = account.Name;
AttentionReason = account.AttentionReason;
Base64ProfilePicture = account.Base64ProfilePictureData;
if (SubMenuItems == null) return;
foreach (var item in SubMenuItems)
{
if (item is IFolderMenuItem folderMenuItem)
{
folderMenuItem.UpdateParentAccounnt(account);
}
}
}
private void UpdateFixAccountIssueMenuItem()
{
if (AttentionReason != AccountAttentionReason.None && !SubMenuItems.Any(a => a is FixAccountIssuesMenuItem))
{
// Add fix issue item if not exists.
SubMenuItems.Insert(0, new FixAccountIssuesMenuItem(Parameter, this));
}
else
{
// Remove existing if issue is resolved.
var fixAccountIssueItem = SubMenuItems.FirstOrDefault(a => a is FixAccountIssuesMenuItem);
if (fixAccountIssueItem != null)
{
SubMenuItems.Remove(fixAccountIssueItem);
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.MenuItems
{
public class FixAccountIssuesMenuItem : MenuItemBase<IMailItemFolder, FolderMenuItem>
{
public MailAccount Account { get; }
public FixAccountIssuesMenuItem(MailAccount account, IMenuItem parentAccountMenuItem) : base(null, null, parentAccountMenuItem)
{
Account = account;
}
}
}

View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.MenuItems
{
public partial class FolderMenuItem : MenuItemBase<IMailItemFolder, FolderMenuItem>, IFolderMenuItem
{
[ObservableProperty]
private int unreadItemCount;
public bool HasTextColor => !string.IsNullOrEmpty(Parameter.TextColorHex);
public bool IsMoveTarget => HandlingFolders.All(a => a.IsMoveTarget);
public SpecialFolderType SpecialFolderType => Parameter.SpecialFolderType;
public bool IsSticky => Parameter.IsSticky;
public bool IsSystemFolder => Parameter.IsSystemFolder;
/// <summary>
/// Display name of the folder. More and Category folders have localized display names.
/// </summary>
public string FolderName
{
get
{
if (Parameter.SpecialFolderType == SpecialFolderType.More)
return Translator.MoreFolderNameOverride;
else if (Parameter.SpecialFolderType == SpecialFolderType.Category)
return Translator.CategoriesFolderNameOverride;
else
return Parameter.FolderName;
}
set => SetProperty(Parameter.FolderName, value, Parameter, (u, n) => u.FolderName = n);
}
public bool IsSynchronizationEnabled
{
get => Parameter.IsSynchronizationEnabled;
set => SetProperty(Parameter.IsSynchronizationEnabled, value, Parameter, (u, n) => u.IsSynchronizationEnabled = n);
}
public IEnumerable<IMailItemFolder> HandlingFolders => new List<IMailItemFolder>() { Parameter };
public MailAccount ParentAccount { get; private set; }
public string AssignedAccountName => ParentAccount?.Name;
public bool ShowUnreadCount => Parameter.ShowUnreadCount;
IEnumerable<IMenuItem> IBaseFolderMenuItem.SubMenuItems => SubMenuItems;
public FolderMenuItem(IMailItemFolder folderStructure, MailAccount parentAccount, IMenuItem parentMenuItem) : base(folderStructure, folderStructure.Id, parentMenuItem)
{
ParentAccount = parentAccount;
}
public void UpdateFolder(IMailItemFolder folder)
{
Parameter = folder;
OnPropertyChanged(nameof(IsSynchronizationEnabled));
OnPropertyChanged(nameof(ShowUnreadCount));
OnPropertyChanged(nameof(HasTextColor));
OnPropertyChanged(nameof(IsSystemFolder));
OnPropertyChanged(nameof(SpecialFolderType));
OnPropertyChanged(nameof(IsSticky));
OnPropertyChanged(nameof(FolderName));
}
public override string ToString() => FolderName;
public void UpdateParentAccounnt(MailAccount account) => ParentAccount = account;
}
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.MenuItems
{
public class ManageAccountsMenuItem : MenuItemBase { }
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.MenuItems
{
public partial class MenuItemBase : ObservableObject, IMenuItem
{
[ObservableProperty]
private bool _isExpanded;
[ObservableProperty]
private bool _isSelected;
public IMenuItem ParentMenuItem { get; }
public Guid? EntityId { get; }
public MenuItemBase(Guid? entityId = null, IMenuItem parentMenuItem = null)
{
EntityId = entityId;
ParentMenuItem = parentMenuItem;
}
public void Expand()
{
// Recursively expand all parent menu items if parent exists, starting from parent.
if (ParentMenuItem != null)
{
IMenuItem parentMenuItem = ParentMenuItem;
while (parentMenuItem != null)
{
parentMenuItem.IsExpanded = true;
parentMenuItem = parentMenuItem.ParentMenuItem;
}
}
// Finally expand itself.
IsExpanded = true;
}
}
public partial class MenuItemBase<T> : MenuItemBase
{
[ObservableProperty]
private T _parameter;
public MenuItemBase(T parameter, Guid? entityId, IMenuItem parentMenuItem = null) : base(entityId, parentMenuItem) => Parameter = parameter;
}
public partial class MenuItemBase<TValue, TCollection> : MenuItemBase<TValue>
{
[ObservableProperty]
private bool _isChildSelected;
protected MenuItemBase(TValue parameter, Guid? entityId, IMenuItem parentMenuItem = null) : base(parameter, entityId, parentMenuItem) { }
public ObservableCollection<TCollection> SubMenuItems { get; set; } = [];
}
}

View File

@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.MenuItems
{
public class MenuItemCollection : ObservableRangeCollection<IMenuItem>
{
// Which types to remove from the list when folders are changing due to selection of new account.
// We don't clear the whole list since we want to keep the New Mail button and account menu items.
private readonly Type[] _preservingTypesForFolderArea = [typeof(AccountMenuItem), typeof(NewMailMenuItem), typeof(MergedAccountMenuItem)];
private readonly IDispatcher _dispatcher;
public MenuItemCollection(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public IEnumerable<IAccountMenuItem> GetAllAccountMenuItems()
{
foreach (var item in this)
{
if (item is MergedAccountMenuItem mergedAccountMenuItem)
{
foreach (var singleItem in mergedAccountMenuItem.SubMenuItems.OfType<IAccountMenuItem>())
{
yield return singleItem;
}
yield return mergedAccountMenuItem;
}
else if (item is IAccountMenuItem accountMenuItem)
yield return accountMenuItem;
}
}
public IEnumerable<IBaseFolderMenuItem> GetAllFolderMenuItems(Guid folderId)
{
foreach (var item in this)
{
if (item is IBaseFolderMenuItem folderMenuItem)
{
if (folderMenuItem.HandlingFolders.Any(a => a.Id == folderId))
{
yield return folderMenuItem;
}
else if (folderMenuItem.SubMenuItems.Any())
{
foreach (var subItem in folderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>())
{
if (subItem.HandlingFolders.Any(a => a.Id == folderId))
{
yield return subItem;
}
}
}
}
}
}
public bool TryGetAccountMenuItem(Guid accountId, out IAccountMenuItem value)
{
value = this.OfType<AccountMenuItem>().FirstOrDefault(a => a.AccountId == accountId);
value ??= this.OfType<MergedAccountMenuItem>().FirstOrDefault(a => a.SubMenuItems.OfType<AccountMenuItem>().Where(b => b.AccountId == accountId) != null);
return value != null;
}
// Pattern: Look for special folder menu item inside the loaded folders for Windows Mail style menu items.
public bool TryGetWindowsStyleRootSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
{
value = this.OfType<IBaseFolderMenuItem>()
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
return value != null;
}
// Pattern: Find the merged account menu item and return the special folder menu item that belongs to the merged account menu item.
// This will not look for the folders inside individual account menu items inside merged account menu item.
public bool TryGetMergedAccountSpecialFolderMenuItem(Guid mergedInboxId, SpecialFolderType specialFolderType, out IBaseFolderMenuItem value)
{
value = this.OfType<MergedAccountFolderMenuItem>()
.Where(a => a.MergedInbox.Id == mergedInboxId)
.FirstOrDefault(a => a.SpecialFolderType == specialFolderType);
return value != null;
}
public bool TryGetFolderMenuItem(Guid folderId, out IBaseFolderMenuItem value)
{
// Root folders
value = this.OfType<IBaseFolderMenuItem>()
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
value ??= this.OfType<FolderMenuItem>()
.SelectMany(a => a.SubMenuItems)
.OfType<IBaseFolderMenuItem>()
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.Id == folderId));
return value != null;
}
public void UpdateUnreadItemCountsToZero()
{
// Handle the root folders.
foreach (var item in this.OfType<IBaseFolderMenuItem>())
{
RecursivelyResetUnreadItemCount(item);
}
}
private void RecursivelyResetUnreadItemCount(IBaseFolderMenuItem baseFolderMenuItem)
{
baseFolderMenuItem.UnreadItemCount = 0;
if (baseFolderMenuItem.SubMenuItems == null) return;
foreach (var subMenuItem in baseFolderMenuItem.SubMenuItems.OfType<IBaseFolderMenuItem>())
{
RecursivelyResetUnreadItemCount(subMenuItem);
}
}
public bool TryGetSpecialFolderMenuItem(Guid accountId, SpecialFolderType specialFolderType, out FolderMenuItem value)
{
value = this.OfType<IBaseFolderMenuItem>()
.FirstOrDefault(a => a.HandlingFolders.Any(b => b.MailAccountId == accountId && b.SpecialFolderType == specialFolderType)) as FolderMenuItem;
return value != null;
}
/// <summary>
/// Skips the merged account menu item, but directly returns the Account menu item inside the merged account menu item.
/// </summary>
/// <param name="accountId">Account id to look for.</param>
/// <returns>Direct AccountMenuItem inside the Merged Account menu item if exists.</returns>
public AccountMenuItem GetSpecificAccountMenuItem(Guid accountId)
{
AccountMenuItem accountMenuItem = null;
accountMenuItem = this.OfType<AccountMenuItem>().FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId));
// Look for the items inside the merged accounts if regular menu item is not found.
accountMenuItem ??= this.OfType<MergedAccountMenuItem>()
.FirstOrDefault(a => a.HoldingAccounts.Any(b => b.Id == accountId))?.SubMenuItems
.OfType<AccountMenuItem>()
.FirstOrDefault(a => a.AccountId == accountId);
return accountMenuItem;
}
public async Task ReplaceFoldersAsync(IEnumerable<IMenuItem> folders)
{
await _dispatcher.ExecuteOnUIThread(() => ClearFolderAreaMenuItems());
await _dispatcher.ExecuteOnUIThread(() => Items.Add(new SeperatorItem()));
await _dispatcher.ExecuteOnUIThread(() => AddRange(folders, System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Enables/disables account menu items in the list.
/// </summary>
/// <param name="isEnabled">Whether menu items should be enabled or disabled.</param>
public async Task SetAccountMenuItemEnabledStatusAsync(bool isEnabled)
{
var accountItems = this.Where(a => a is IAccountMenuItem).Cast<IAccountMenuItem>();
await _dispatcher.ExecuteOnUIThread(() =>
{
foreach (var item in accountItems)
{
item.IsEnabled = isEnabled;
}
});
}
public void AddAccountMenuItem(IAccountMenuItem accountMenuItem)
{
var lastAccount = Items.OfType<IAccountMenuItem>().LastOrDefault();
// Index 0 is always the New Mail button.
var insertIndex = lastAccount == null ? 1 : Items.IndexOf(lastAccount) + 1;
Insert(insertIndex, accountMenuItem);
}
private void ClearFolderAreaMenuItems()
{
var itemsToRemove = this.Where(a => !_preservingTypesForFolderArea.Contains(a.GetType())).ToList();
itemsToRemove.ForEach(item =>
{
item.IsExpanded = false;
item.IsSelected = false;
try
{
Remove(item);
}
catch (Exception) { }
});
}
}
}

View File

@@ -0,0 +1,112 @@
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.MenuItems
{
/// <summary>
/// Menu item that holds a list of folders under the merged account menu item.
/// </summary>
public partial class MergedAccountFolderMenuItem : MenuItemBase<List<IMailItemFolder>, IMenuItem>, IMergedAccountFolderMenuItem
{
public SpecialFolderType FolderType { get; }
public string FolderName { get; private set; }
// Any of the folders is enough to determine the synchronization enable/disable state.
public bool IsSynchronizationEnabled => HandlingFolders.Any(a => a.IsSynchronizationEnabled);
public bool IsMoveTarget => HandlingFolders.All(a => a.IsMoveTarget);
public IEnumerable<IMailItemFolder> HandlingFolders => Parameter;
// All folders in the list should have the same type.
public SpecialFolderType SpecialFolderType => HandlingFolders.First().SpecialFolderType;
public bool IsSticky => true;
public bool IsSystemFolder => true;
public string AssignedAccountName => MergedInbox?.Name;
public MergedInbox MergedInbox { get; set; }
public bool ShowUnreadCount => HandlingFolders?.Any(a => a.ShowUnreadCount) ?? false;
public new IEnumerable<IMenuItem> SubMenuItems => base.SubMenuItems;
[ObservableProperty]
private int unreadItemCount;
// Merged account's shared folder menu item does not have an entity id.
// Navigations to specific folders are done by explicit folder id if needed.
public MergedAccountFolderMenuItem(List<IMailItemFolder> parameter, IMenuItem parentMenuItem, MergedInbox mergedInbox) : base(parameter, null, parentMenuItem)
{
Guard.IsNotNull(mergedInbox, nameof(mergedInbox));
Guard.IsNotNull(parameter, nameof(parameter));
Guard.HasSizeGreaterThan(parameter, 0, nameof(parameter));
MergedInbox = mergedInbox;
SetFolderName();
// All folders in the list should have the same type.
FolderType = parameter[0].SpecialFolderType;
}
private void SetFolderName()
{
// Folders that hold more than 1 folder belong to merged account.
// These folders will be displayed as their localized names based on the
// special type they have.
if (HandlingFolders.Count() > 1)
{
FolderName = GetSpecialFolderName(HandlingFolders.First());
}
else
{
// Folder only holds 1 Id, but it's displayed as merged account folder.
FolderName = HandlingFolders.First().FolderName;
}
}
private string GetSpecialFolderName(IMailItemFolder folder)
{
var specialType = folder.SpecialFolderType;
// We only handle 5 different types for combining folders.
// Rest of the types are not supported.
return specialType switch
{
SpecialFolderType.Inbox => Translator.MergedAccountCommonFolderInbox,
SpecialFolderType.Draft => Translator.MergedAccountCommonFolderDraft,
SpecialFolderType.Sent => Translator.MergedAccountCommonFolderSent,
SpecialFolderType.Deleted => Translator.MergedAccountCommonFolderTrash,
SpecialFolderType.Junk => Translator.MergedAccountCommonFolderJunk,
SpecialFolderType.Archive => Translator.MergedAccountCommonFolderArchive,
_ => folder.FolderName,
};
}
public void UpdateFolder(IMailItemFolder folder)
{
var existingFolder = Parameter.FirstOrDefault(a => a.Id == folder.Id);
if (existingFolder == null) return;
Parameter.Remove(existingFolder);
Parameter.Add(folder);
SetFolderName();
OnPropertyChanged(nameof(ShowUnreadCount));
OnPropertyChanged(nameof(IsSynchronizationEnabled));
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.MenuItems
{
public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem>, IMergedAccountMenuItem
{
public int MergedAccountCount => HoldingAccounts?.Count() ?? 0;
public IEnumerable<MailAccount> HoldingAccounts { get; }
[ObservableProperty]
private int unreadItemCount;
[ObservableProperty]
private double synchronizationProgress;
[ObservableProperty]
private string mergedAccountName;
[ObservableProperty]
private bool _isEnabled = true;
public MergedAccountMenuItem(MergedInbox mergedInbox, IEnumerable<MailAccount> holdingAccounts, IMenuItem parent) : base(mergedInbox, mergedInbox.Id, parent)
{
MergedAccountName = mergedInbox.Name;
HoldingAccounts = holdingAccounts;
}
public void RefreshFolderItemCount()
{
UnreadItemCount = SubMenuItems.OfType<IAccountMenuItem>().Sum(a => a.UnreadItemCount);
}
public void UpdateAccount(MailAccount account)
{
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.MenuItems
{
public class MergedAccountMoreFolderMenuItem : MenuItemBase<object, IMenuItem>
{
public MergedAccountMoreFolderMenuItem(object parameter, Guid? entityId, IMenuItem parentMenuItem = null) : base(parameter, entityId, parentMenuItem)
{
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.MenuItems
{
public class NewMailMenuItem : MenuItemBase { }
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.MenuItems
{
public class RateMenuItem : MenuItemBase { }
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.MenuItems
{
public class SeperatorItem : MenuItemBase { }
}

View File

@@ -0,0 +1,4 @@
namespace Wino.Core.Domain.MenuItems
{
public class SettingsItem : MenuItemBase { }
}

View File

@@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.IO;
using MimeKit;
using MimeKit.Text;
using MimeKit.Tnef;
namespace Wino.Core.Domain.Models.MailItem
{
/// <summary>
/// Visits a MimeMessage and generates HTML suitable to be rendered by a browser control.
/// </summary>
public class HtmlPreviewVisitor : MimeVisitor
{
List<MultipartRelated> stack = new List<MultipartRelated>();
List<MimeEntity> attachments = new List<MimeEntity>();
readonly string tempDir;
public string Body { get; set; }
/// <summary>
/// Creates a new HtmlPreviewVisitor.
/// </summary>
/// <param name="tempDirectory">A temporary directory used for storing image files.</param>
public HtmlPreviewVisitor(string tempDirectory)
{
tempDir = tempDirectory;
}
/// <summary>
/// The list of attachments that were in the MimeMessage.
/// </summary>
public IList<MimeEntity> Attachments
{
get { return attachments; }
}
/// <summary>
/// The HTML string that can be set on the BrowserControl.
/// </summary>
public string HtmlBody
{
get { return Body ?? string.Empty; }
}
protected override void VisitMultipartAlternative(MultipartAlternative alternative)
{
// walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful
for (int i = alternative.Count - 1; i >= 0 && Body == null; i--)
alternative[i].Accept(this);
}
protected override void VisitMultipartRelated(MultipartRelated related)
{
var root = related.Root;
// push this multipart/related onto our stack
stack.Add(related);
// visit the root document
root.Accept(this);
// pop this multipart/related off our stack
stack.RemoveAt(stack.Count - 1);
}
// look up the image based on the img src url within our multipart/related stack
bool TryGetImage(string url, out MimePart image)
{
UriKind kind;
int index;
Uri uri;
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
kind = UriKind.Absolute;
else if (Uri.IsWellFormedUriString(url, UriKind.Relative))
kind = UriKind.Relative;
else
kind = UriKind.RelativeOrAbsolute;
try
{
uri = new Uri(url, kind);
}
catch
{
image = null;
return false;
}
for (int i = stack.Count - 1; i >= 0; i--)
{
if ((index = stack[i].IndexOf(uri)) == -1)
continue;
image = stack[i][index] as MimePart;
return image != null;
}
image = null;
return false;
}
// Save the image to our temp directory and return a "file://" url suitable for
// the browser control to load.
// Note: if you'd rather embed the image data into the HTML, you can construct a
// "data:" url instead.
string SaveImage(MimePart image)
{
using (var memory = new MemoryStream())
{
image.Content.DecodeTo(memory);
var buffer = memory.GetBuffer();
var length = (int)memory.Length;
var base64 = Convert.ToBase64String(buffer, 0, length);
return string.Format("data:{0};base64,{1}", image.ContentType.MimeType, base64);
}
//string fileName = url
// .Replace(':', '_')
// .Replace('\\', '_')
// .Replace('/', '_');
//string path = Path.Combine(tempDir, fileName);
//if (!File.Exists(path))
//{
// using (var output = File.Create(path))
// image.Content.DecodeTo(output);
//}
//return "file://" + path.Replace('\\', '/');
}
// Replaces <img src=...> urls that refer to images embedded within the message with
// "file://" urls that the browser control will actually be able to load.
void HtmlTagCallback(HtmlTagContext ctx, HtmlWriter htmlWriter)
{
if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0)
{
ctx.WriteTag(htmlWriter, false);
// replace the src attribute with a file:// URL
foreach (var attribute in ctx.Attributes)
{
if (attribute.Id == HtmlAttributeId.Src)
{
MimePart image;
string url;
if (!TryGetImage(attribute.Value, out image))
{
htmlWriter.WriteAttribute(attribute);
continue;
}
url = SaveImage(image);
htmlWriter.WriteAttributeName(attribute.Name);
htmlWriter.WriteAttributeValue(url);
}
else
{
htmlWriter.WriteAttribute(attribute);
}
}
}
else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag)
{
ctx.WriteTag(htmlWriter, false);
// add and/or replace oncontextmenu="return false;"
foreach (var attribute in ctx.Attributes)
{
if (attribute.Name.ToLowerInvariant() == "oncontextmenu")
continue;
htmlWriter.WriteAttribute(attribute);
}
htmlWriter.WriteAttribute("oncontextmenu", "return false;");
}
else
{
if (ctx.TagId == HtmlTagId.Unknown)
{
ctx.DeleteTag = true;
ctx.DeleteEndTag = true;
}
else
{
ctx.WriteTag(htmlWriter, true);
}
}
}
protected override void VisitTextPart(TextPart entity)
{
TextConverter converter;
if (Body != null)
{
// since we've already found the body, treat this as an attachment
attachments.Add(entity);
return;
}
if (entity.IsHtml)
{
converter = new HtmlToHtml
{
HtmlTagCallback = HtmlTagCallback
};
}
else if (entity.IsFlowed)
{
var flowed = new FlowedToHtml();
string delsp;
if (entity.ContentType.Parameters.TryGetValue("delsp", out delsp))
flowed.DeleteSpace = delsp.ToLowerInvariant() == "yes";
converter = flowed;
}
else
{
converter = new TextToHtml();
}
Body = converter.Convert(entity.Text);
}
protected override void VisitTnefPart(TnefPart entity)
{
// extract any attachments in the MS-TNEF part
attachments.AddRange(entity.ExtractAttachments());
}
protected override void VisitMessagePart(MessagePart entity)
{
// treat message/rfc822 parts as attachments
attachments.Add(entity);
}
protected override void VisitMimePart(MimePart entity)
{
// realistically, if we've gotten this far, then we can treat this as an attachment
// even if the IsAttachment property is false.
attachments.Add(entity);
}
}
}

View File

@@ -5,6 +5,8 @@
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<LangVersion>12.0</LangVersion>
<Platforms>AnyCPU;x64;x86</Platforms>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
@@ -58,6 +60,8 @@
<EmbeddedResource Include="Translations\zh_CN\resources.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.3.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>