Demo contacts page.
This commit is contained in:
@@ -38,6 +38,8 @@
|
||||
- Place ViewModels in appropriate projects (Wino.Mail.ViewModels, Wino.Core.ViewModels, etc.)
|
||||
- Create abstract page classes in Views/Abstract folders
|
||||
- Follow the existing naming conventions
|
||||
- **NEVER** edit files in the old Wino.Mail project folder - it's UWP and deprecated
|
||||
- **ALWAYS** work with WinUI projects (Wino.Mail.WinUI, Wino.Core.WinUI) for UI components
|
||||
|
||||
### Error Handling
|
||||
- Always wrap async operations in try-catch blocks
|
||||
@@ -56,6 +58,15 @@
|
||||
- Use PersonPicture controls for contact avatars
|
||||
- Support multiple selection where appropriate
|
||||
|
||||
### UI Data Binding and Converters
|
||||
- **NEVER** create IValueConverter classes or add them to Converters.xaml
|
||||
- **NEVER** use BoolToVisibilityConverter - boolean values are automatically converted to Visibility by the WinUI 3 SDK
|
||||
- **ALWAYS** use XamlHelpers static methods for data transformations when needed
|
||||
- Use XamlHelpers methods with x:Bind syntax: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(PropertyName), Mode=OneWay}`
|
||||
- Available XamlHelpers methods include: ReverseBoolToVisibilityConverter, CountToBooleanConverter, BoolToSelectionMode, Base64ToBitmapImage, etc.
|
||||
- If a helper method doesn't exist, add it to XamlHelpers.cs instead of creating a converter
|
||||
- For boolean to Visibility binding, use direct binding: `Visibility="{x:Bind BooleanProperty, Mode=OneWay}"`
|
||||
|
||||
## Code Style
|
||||
- Use var where type is obvious
|
||||
- Use string interpolation over string.Format where simple
|
||||
|
||||
@@ -36,6 +36,12 @@ public class AccountContact : IEquatable<AccountContact>
|
||||
/// </summary>
|
||||
public bool IsRootContact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, indicates that the contact has been manually modified by the user.
|
||||
/// Contacts with this flag set to true should not be updated during synchronization.
|
||||
/// </summary>
|
||||
public bool IsOverridden { get; set; } = false;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return Equals(obj as AccountContact);
|
||||
|
||||
@@ -9,6 +9,7 @@ public enum WinoPage
|
||||
IdlePage,
|
||||
ComposePage,
|
||||
SettingsPage,
|
||||
ContactsPage,
|
||||
MailRenderingPage,
|
||||
WelcomePage,
|
||||
AccountDetailsPage,
|
||||
@@ -27,7 +28,6 @@ public enum WinoPage
|
||||
AliasManagementPage,
|
||||
EditAccountDetailsPage,
|
||||
KeyboardShortcutsPage,
|
||||
// Calendar
|
||||
CalendarPage,
|
||||
CalendarSettingsPage,
|
||||
EventDetailsPage
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
@@ -11,4 +12,11 @@ public interface IContactService
|
||||
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
|
||||
Task SaveAddressInformationAsync(MimeMessage message);
|
||||
Task<AccountContact> CreateNewContactAsync(string address, string displayName);
|
||||
|
||||
// New methods for ContactsPage
|
||||
Task<List<AccountContact>> GetAllContactsAsync();
|
||||
Task<List<AccountContact>> SearchContactsAsync(string searchQuery);
|
||||
Task<AccountContact> UpdateContactAsync(AccountContact contact);
|
||||
Task DeleteContactAsync(string address);
|
||||
Task DeleteContactsAsync(IEnumerable<string> addresses);
|
||||
}
|
||||
|
||||
@@ -59,4 +59,11 @@ public interface IMailDialogService : IDialogServiceBase
|
||||
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
|
||||
Task<KeyboardShortcutDialogResult> ShowKeyboardShortcutDialogAsync(KeyboardShortcut existingShortcut = null);
|
||||
#pragma warning restore CS8625
|
||||
|
||||
/// <summary>
|
||||
/// Presents a dialog to the user for contact creation/modification.
|
||||
/// </summary>
|
||||
/// <param name="contact">Existing contact to edit, or null for new contact.</param>
|
||||
/// <returns>Contact information. Null if canceled.</returns>
|
||||
Task<AccountContact> ShowEditContactDialogAsync(AccountContact contact = null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Wino.Core.Domain.MenuItems;
|
||||
|
||||
public class ContactsMenuItem : MenuItemBase { }
|
||||
@@ -723,7 +723,39 @@
|
||||
"WinoUpgradeRemainingAccountsMessage": "{0} out of {1} free accounts used.",
|
||||
"Yesterday": "Yesterday",
|
||||
"SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval",
|
||||
"SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail."
|
||||
"SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.",
|
||||
"ContactsPage_Title": "Contacts",
|
||||
"ContactsPage_AddContact": "Add Contact",
|
||||
"ContactsPage_EditContact": "Edit Contact",
|
||||
"ContactsPage_DeleteContact": "Delete Contact",
|
||||
"ContactsPage_SearchPlaceholder": "Search contacts...",
|
||||
"ContactsPage_NoContacts": "No contacts found",
|
||||
"ContactsPage_ContactsCount": "{0} contacts",
|
||||
"ContactsPage_SelectedContactsCount": "{0} selected",
|
||||
"ContactsPage_DeleteSelectedContacts": "Delete Selected",
|
||||
"ContactEditDialog_Title": "Edit Contact",
|
||||
"ContactEditDialog_PhotoSection": "Photo",
|
||||
"ContactEditDialog_ChoosePhoto": "Choose Photo",
|
||||
"ContactEditDialog_RemovePhoto": "Remove Photo",
|
||||
"ContactEditDialog_NameHeader": "Name",
|
||||
"ContactEditDialog_NamePlaceholder": "Contact name",
|
||||
"ContactEditDialog_EmailHeader": "Email Address",
|
||||
"ContactEditDialog_EmailPlaceholder": "contact@example.com",
|
||||
"ContactEditDialog_InfoSection": "Contact Information",
|
||||
"ContactEditDialog_RootContactInfo": "This is a root contact associated with your accounts and cannot be deleted.",
|
||||
"ContactEditDialog_OverriddenContactInfo": "This contact has been manually modified and will not be updated during synchronization.",
|
||||
"ContactsPage_Subtitle": "Manage your email contacts and their information",
|
||||
"ContactStatus_Account": "Account",
|
||||
"ContactStatus_Modified": "Modified",
|
||||
"ContactAction_Edit": "Edit contact",
|
||||
"ContactAction_ChangePhoto": "Change photo",
|
||||
"ContactAction_Delete": "Delete contact",
|
||||
"ContactAction_Add": "Add Contact",
|
||||
"ContactSelection_Selected": "selected",
|
||||
"ContactSelection_SelectAll": "Select All",
|
||||
"ContactSelection_Clear": "Clear Selection",
|
||||
"ContactsPage_EmptyState": "No contacts to display",
|
||||
"ContactsPage_AddFirstContact": "Add your first contact"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,18 +16,18 @@ public class MailOperationViewModel
|
||||
{
|
||||
return Operation switch
|
||||
{
|
||||
MailOperation.Archive => "Archive",
|
||||
MailOperation.UnArchive => "Unarchive",
|
||||
MailOperation.SoftDelete => "Delete",
|
||||
MailOperation.Move => "Move",
|
||||
MailOperation.MoveToJunk => "Move to Junk",
|
||||
MailOperation.SetFlag => "Set Flag",
|
||||
MailOperation.ClearFlag => "Clear Flag",
|
||||
MailOperation.MarkAsRead => "Mark as Read",
|
||||
MailOperation.MarkAsUnread => "Mark as Unread",
|
||||
MailOperation.Reply => "Reply",
|
||||
MailOperation.ReplyAll => "Reply All",
|
||||
MailOperation.Forward => "Forward",
|
||||
MailOperation.Archive => Translator.MailOperation_Archive,
|
||||
MailOperation.UnArchive => Translator.MailOperation_Unarchive,
|
||||
MailOperation.SoftDelete => Translator.MailOperation_Delete,
|
||||
MailOperation.Move => Translator.MailOperation_Move,
|
||||
MailOperation.MoveToJunk => Translator.MailOperation_MoveJunk,
|
||||
MailOperation.SetFlag => Translator.MailOperation_SetFlag,
|
||||
MailOperation.ClearFlag => Translator.MailOperation_ClearFlag,
|
||||
MailOperation.MarkAsRead => Translator.MailOperation_MarkAsRead,
|
||||
MailOperation.MarkAsUnread => Translator.MailOperation_MarkAsUnread,
|
||||
MailOperation.Reply => Translator.MailOperation_Reply,
|
||||
MailOperation.ReplyAll => Translator.MailOperation_ReplyAll,
|
||||
MailOperation.Forward => Translator.MailOperation_Forward,
|
||||
_ => Operation.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI;
|
||||
@@ -46,6 +47,27 @@ public static class XamlHelpers
|
||||
public static bool ObjectEquals(object obj1, object obj2) => object.Equals(obj1, obj2);
|
||||
public static Visibility CountToVisibilityConverter(int value) => value > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
public static Visibility CountToVisibilityConverterWithThreshold(int value, int threshold) => value > threshold ? Visibility.Visible : Visibility.Collapsed;
|
||||
public static ListViewSelectionMode BoolToSelectionMode(bool isSelectionMode) => isSelectionMode ? ListViewSelectionMode.Multiple : ListViewSelectionMode.None;
|
||||
public static string BoolToSelectionModeText(bool isSelectionMode) => isSelectionMode ? Translator.Buttons_Cancel : Translator.Buttons_Multiselect;
|
||||
|
||||
public static Microsoft.UI.Xaml.Media.Imaging.BitmapImage Base64ToBitmapImage(string base64String)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64String))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var imageBytes = Convert.FromBase64String(base64String);
|
||||
using var stream = new System.IO.MemoryStream(imageBytes);
|
||||
var bitmap = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage();
|
||||
bitmap.SetSource(stream.AsRandomAccessStream());
|
||||
return bitmap;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static InfoBarSeverity InfoBarSeverityConverter(InfoBarMessageType messageType)
|
||||
{
|
||||
return messageType switch
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Wino.Core.WinUI.Selectors;
|
||||
public partial class NavigationMenuTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate MenuItemTemplate { get; set; }
|
||||
public DataTemplate ContactsMenuItemTemplate { get; set; }
|
||||
public DataTemplate AccountManagementTemplate { get; set; }
|
||||
public DataTemplate ClickableAccountMenuTemplate { get; set; }
|
||||
public DataTemplate MergedAccountTemplate { get; set; }
|
||||
@@ -27,6 +28,8 @@ public partial class NavigationMenuTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
if (item is NewMailMenuItem)
|
||||
return NewMailTemplate;
|
||||
else if (item is ContactsMenuItem)
|
||||
return ContactsMenuItemTemplate;
|
||||
else if (item is SettingsItem)
|
||||
return SettingsItemTemplate;
|
||||
else if (item is SeperatorItem)
|
||||
|
||||
@@ -19,6 +19,7 @@ using Wino.Core.WinUI.Dialogs;
|
||||
using Wino.Core.WinUI.Extensions;
|
||||
using Wino.Dialogs;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Wino.Core.WinUI.Services;
|
||||
|
||||
@@ -52,6 +53,9 @@ public class DialogServiceBase : IDialogServiceBase
|
||||
|
||||
picker.FileTypeFilter.Add("*");
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var folder = await picker.PickSingleFolderAsync();
|
||||
if (folder == null) return string.Empty;
|
||||
|
||||
@@ -89,6 +93,9 @@ public class DialogServiceBase : IDialogServiceBase
|
||||
picker.FileTypeFilter.Add(filter.ToString());
|
||||
}
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var files = await picker.PickMultipleFilesAsync();
|
||||
if (files == null) return returnList;
|
||||
|
||||
@@ -115,6 +122,9 @@ public class DialogServiceBase : IDialogServiceBase
|
||||
picker.FileTypeFilter.Add(filter.ToString());
|
||||
}
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var file = await picker.PickSingleFileAsync();
|
||||
|
||||
if (file == null) return null;
|
||||
@@ -259,6 +269,9 @@ public class DialogServiceBase : IDialogServiceBase
|
||||
|
||||
picker.FileTypeFilter.Add("*");
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var pickedFolder = await picker.PickSingleFolderAsync();
|
||||
|
||||
if (pickedFolder != null)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -54,6 +54,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
private readonly SettingsItem SettingsItem = new SettingsItem();
|
||||
private readonly ManageAccountsMenuItem ManageAccountsMenuItem = new ManageAccountsMenuItem();
|
||||
private readonly ContactsMenuItem ContactsMenuItem = new ContactsMenuItem();
|
||||
|
||||
public IMenuItem CreateMailMenuItem = new NewMailMenuItem();
|
||||
|
||||
@@ -150,6 +151,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
|
||||
FooterItems.Clear();
|
||||
|
||||
FooterItems.Add(ContactsMenuItem);
|
||||
FooterItems.Add(ManageAccountsMenuItem);
|
||||
FooterItems.Add(SettingsItem);
|
||||
});
|
||||
@@ -591,6 +593,10 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.ManageAccountsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
||||
}
|
||||
else if (clickedMenuItem is ContactsMenuItem)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.ContactsPage, parameter, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
|
||||
}
|
||||
else if (clickedMenuItem is IAccountMenuItem clickedAccountMenuItem)
|
||||
{
|
||||
// Changing loaded account.
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
|
||||
public partial class ContactsPageViewModel : MailBaseViewModel
|
||||
{
|
||||
private readonly IContactService _contactService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
|
||||
private List<AccountContact> _allContacts = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SearchQuery { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelectionMode { get; set; } = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedContactsCount { get; set; } = 0;
|
||||
|
||||
public ObservableCollection<AccountContact> Contacts { get; } = new();
|
||||
public ObservableCollection<AccountContact> SelectedContacts { get; } = new();
|
||||
|
||||
public ContactsPageViewModel(IContactService contactService, IMailDialogService dialogService)
|
||||
{
|
||||
_contactService = contactService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
|
||||
}
|
||||
|
||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedTo(mode, parameters);
|
||||
|
||||
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
||||
SelectedContacts.CollectionChanged += SelectedContactsChanged;
|
||||
|
||||
await LoadContactsAsync();
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
|
||||
{
|
||||
base.OnNavigatedFrom(mode, parameters);
|
||||
|
||||
SelectedContacts.CollectionChanged -= SelectedContactsChanged;
|
||||
}
|
||||
|
||||
private void SelectedContactsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
=> SelectedContactsCount = SelectedContacts.Count;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadContactsAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
_allContacts = await _contactService.GetAllContactsAsync();
|
||||
await FilterContactsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Error", $"Failed to load contacts: {ex.Message}", InfoBarMessageType.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SearchContactsAsync()
|
||||
{
|
||||
await FilterContactsAsync();
|
||||
}
|
||||
|
||||
private async Task FilterContactsAsync()
|
||||
{
|
||||
List<AccountContact> filteredContacts;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SearchQuery))
|
||||
{
|
||||
filteredContacts = _allContacts;
|
||||
}
|
||||
else
|
||||
{
|
||||
filteredContacts = await _contactService.SearchContactsAsync(SearchQuery);
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
Contacts.Clear();
|
||||
foreach (var contact in filteredContacts.OrderBy(c => c.Name ?? c.Address))
|
||||
{
|
||||
Contacts.Add(contact);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddContactAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowEditContactDialogAsync(null);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newContact = await _contactService.CreateNewContactAsync(result.Address, result.Name);
|
||||
|
||||
if (!string.IsNullOrEmpty(result.Base64ContactPicture))
|
||||
{
|
||||
newContact.Base64ContactPicture = result.Base64ContactPicture;
|
||||
await _contactService.UpdateContactAsync(newContact);
|
||||
}
|
||||
|
||||
_allContacts.Add(newContact);
|
||||
await FilterContactsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage("Success", "Contact added successfully", InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Error", $"Failed to add contact: {ex.Message}", InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EditContactAsync(AccountContact contact)
|
||||
{
|
||||
if (contact == null) return;
|
||||
|
||||
var result = await _dialogService.ShowEditContactDialogAsync(contact);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Update the contact properties
|
||||
contact.Name = result.Name;
|
||||
contact.Base64ContactPicture = result.Base64ContactPicture;
|
||||
contact.IsOverridden = result.IsOverridden;
|
||||
|
||||
await _contactService.UpdateContactAsync(contact);
|
||||
|
||||
// Update the UI
|
||||
var index = _allContacts.FindIndex(c => c.Address == contact.Address);
|
||||
if (index >= 0)
|
||||
{
|
||||
_allContacts[index] = contact;
|
||||
}
|
||||
|
||||
await FilterContactsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage("Success", "Contact updated successfully", InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Error", $"Failed to update contact: {ex.Message}", InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteContactAsync(AccountContact contact)
|
||||
{
|
||||
if (contact == null || contact.IsRootContact)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Cannot Delete", "Root contacts cannot be deleted", InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _dialogService.ShowConfirmationDialogAsync(
|
||||
$"Are you sure you want to delete the contact '{contact.Name ?? contact.Address}'?",
|
||||
"Delete Contact",
|
||||
"Delete");
|
||||
|
||||
if (result)
|
||||
{
|
||||
await DeleteContactsInternalAsync(new[] { contact });
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteSelectedContactsAsync()
|
||||
{
|
||||
if (SelectedContacts.Count == 0) return;
|
||||
|
||||
var deletableContacts = SelectedContacts.Where(c => !c.IsRootContact).ToList();
|
||||
|
||||
if (deletableContacts.Count == 0)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Cannot Delete", "Root contacts cannot be deleted", InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _dialogService.ShowConfirmationDialogAsync(
|
||||
$"Are you sure you want to delete {deletableContacts.Count} contact(s)?",
|
||||
"Delete Contacts",
|
||||
"Delete");
|
||||
|
||||
if (result)
|
||||
{
|
||||
await DeleteContactsInternalAsync(deletableContacts);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteContactsInternalAsync(IEnumerable<AccountContact> contactsToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addresses = contactsToDelete.Select(c => c.Address);
|
||||
await _contactService.DeleteContactsAsync(addresses);
|
||||
|
||||
// Update local collections
|
||||
foreach (var contact in contactsToDelete.ToList())
|
||||
{
|
||||
_allContacts.Remove(contact);
|
||||
SelectedContacts.Remove(contact);
|
||||
}
|
||||
|
||||
await FilterContactsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage("Success", "Contacts deleted successfully", InfoBarMessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Error", $"Failed to delete contacts: {ex.Message}", InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleSelection()
|
||||
{
|
||||
IsSelectionMode = !IsSelectionMode;
|
||||
|
||||
if (!IsSelectionMode)
|
||||
{
|
||||
SelectedContacts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectAllContacts()
|
||||
{
|
||||
SelectedContacts.Clear();
|
||||
foreach (var contact in Contacts)
|
||||
{
|
||||
SelectedContacts.Add(contact);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearSelection()
|
||||
{
|
||||
SelectedContacts.Clear();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task PickContactPhotoAsync(AccountContact contact)
|
||||
{
|
||||
if (contact == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var files = await _dialogService.PickFilesAsync(".png", ".jpg", ".jpeg");
|
||||
|
||||
if (files?.Any() == true)
|
||||
{
|
||||
var file = files.First();
|
||||
var base64Image = Convert.ToBase64String(file.Data);
|
||||
|
||||
contact.Base64ContactPicture = base64Image;
|
||||
await _contactService.UpdateContactAsync(contact);
|
||||
|
||||
await FilterContactsAsync();
|
||||
_dialogService.InfoBarMessage("Success", "Contact photo updated successfully", InfoBarMessageType.Success);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.InfoBarMessage("Error", $"Failed to update photo: {ex.Message}", InfoBarMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSearchQueryChanged(string value)
|
||||
{
|
||||
// Debounce search - implement if needed
|
||||
SearchContactsCommand.ExecuteAsync(null);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
|
||||
services.AddTransient(typeof(LanguageTimePageViewModel));
|
||||
services.AddTransient(typeof(AppPreferencesPageViewModel));
|
||||
services.AddTransient(typeof(AliasManagementPageViewModel));
|
||||
services.AddTransient(typeof(ContactsPageViewModel));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -340,6 +340,7 @@
|
||||
x:Key="NavigationMenuTemplateSelector"
|
||||
AccountManagementTemplate="{StaticResource ManageAccountsTemplate}"
|
||||
ClickableAccountMenuTemplate="{StaticResource ClickableAccountMenuTemplate}"
|
||||
ContactsMenuItemTemplate="{StaticResource ContactsTemplate}"
|
||||
FixAuthenticationIssueTemplate="{StaticResource FixAuthenticationIssueTemplate}"
|
||||
FixMissingFolderConfigTemplate="{StaticResource FixMissingFolderConfig}"
|
||||
FolderMenuTemplate="{StaticResource FolderMenuTemplate}"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<ContentDialog
|
||||
x:Class="Wino.Dialogs.ContactEditDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="Edit Contact"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
DefaultButton="Primary"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
PrimaryButtonClick="SaveClicked"
|
||||
PrimaryButtonText="Save"
|
||||
SecondaryButtonClick="CancelClicked"
|
||||
SecondaryButtonText="Cancel"
|
||||
Style="{StaticResource WinoDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">400</x:Double>
|
||||
</ContentDialog.Resources>
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Contact Name -->
|
||||
<TextBox
|
||||
x:Name="ContactNameTextBox"
|
||||
Header="Name"
|
||||
PlaceholderText="Contact name"
|
||||
TextChanged="ValidateInput" />
|
||||
|
||||
<!-- Email Address -->
|
||||
<TextBox
|
||||
x:Name="EmailAddressTextBox"
|
||||
Header="Email Address"
|
||||
PlaceholderText="contact@example.com"
|
||||
TextChanged="ValidateInput" />
|
||||
|
||||
<!-- Contact Photo -->
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,0,0,8"
|
||||
FontWeight="SemiBold"
|
||||
Text="Photo" />
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<PersonPicture
|
||||
x:Name="ContactPhotoPersonPicture"
|
||||
Grid.Column="0"
|
||||
Width="64"
|
||||
Height="64"
|
||||
Margin="0,0,16,0" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button
|
||||
x:Name="ChoosePhotoButton"
|
||||
Click="ChoosePhotoClicked"
|
||||
Content="Choose Photo" />
|
||||
<Button
|
||||
x:Name="RemovePhotoButton"
|
||||
Click="RemovePhotoClicked"
|
||||
Content="Remove Photo" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Contact Status Info -->
|
||||
<Border
|
||||
x:Name="RootContactInfoBorder"
|
||||
Padding="12,8"
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="4"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="This is a root contact and cannot be deleted."
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
x:Name="OverriddenContactInfoBorder"
|
||||
Padding="12,8"
|
||||
Background="{ThemeResource SystemFillColorCautionBrush}"
|
||||
CornerRadius="4"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="This contact has been manually modified."
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Dialogs;
|
||||
|
||||
public sealed partial class ContactEditDialog : ContentDialog
|
||||
{
|
||||
private AccountContact _contact;
|
||||
private IDialogServiceBase? _dialogService;
|
||||
|
||||
public AccountContact Contact => _contact;
|
||||
|
||||
public ContactEditDialog(AccountContact? contact = null, IDialogServiceBase? dialogService = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_contact = contact ?? new AccountContact();
|
||||
_dialogService = dialogService;
|
||||
|
||||
LoadContactData();
|
||||
ValidateInput();
|
||||
}
|
||||
|
||||
private void LoadContactData()
|
||||
{
|
||||
if (_contact != null)
|
||||
{
|
||||
ContactNameTextBox.Text = _contact.Name ?? string.Empty;
|
||||
EmailAddressTextBox.Text = _contact.Address ?? string.Empty;
|
||||
|
||||
// Show info badges
|
||||
if (_contact.IsRootContact)
|
||||
{
|
||||
RootContactInfoBorder.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
if (_contact.IsOverridden)
|
||||
{
|
||||
OverriddenContactInfoBorder.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ChoosePhotoClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// TODO: Implement photo picker
|
||||
}
|
||||
|
||||
private void RemovePhotoClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ContactPhotoPersonPicture.ProfilePicture = null;
|
||||
RemovePhotoButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ValidateInput(object? sender = null, TextChangedEventArgs? e = null)
|
||||
{
|
||||
var hasName = !string.IsNullOrWhiteSpace(ContactNameTextBox.Text);
|
||||
var hasEmail = !string.IsNullOrWhiteSpace(EmailAddressTextBox.Text);
|
||||
var isValidEmail = hasEmail && IsValidEmail(EmailAddressTextBox.Text);
|
||||
|
||||
IsPrimaryButtonEnabled = hasName && isValidEmail;
|
||||
}
|
||||
|
||||
private bool IsValidEmail(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addr = new System.Net.Mail.MailAddress(email);
|
||||
return addr.Address == email;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
// Update contact data
|
||||
_contact.Name = ContactNameTextBox.Text?.Trim();
|
||||
_contact.Address = EmailAddressTextBox.Text?.Trim();
|
||||
|
||||
// Mark as overridden if this was a user edit
|
||||
if (!string.IsNullOrEmpty(_contact.Address))
|
||||
{
|
||||
_contact.IsOverridden = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
// Nothing to do, dialog will close
|
||||
}
|
||||
}
|
||||
@@ -209,4 +209,21 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
||||
|
||||
return dialog.Result;
|
||||
}
|
||||
|
||||
public async Task<Core.Domain.Entities.Shared.AccountContact?> ShowEditContactDialogAsync(Core.Domain.Entities.Shared.AccountContact? contact = null)
|
||||
{
|
||||
var dialog = new ContactEditDialog(contact, this)
|
||||
{
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||
};
|
||||
|
||||
var result = await HandleDialogPresentationAsync(dialog);
|
||||
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
return dialog.Contact;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
_statePersistanceService = statePersistanceService;
|
||||
}
|
||||
|
||||
public Type GetPageType(WinoPage winoPage)
|
||||
public Type? GetPageType(WinoPage winoPage)
|
||||
{
|
||||
return winoPage switch
|
||||
{
|
||||
@@ -60,6 +60,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.LanguageTimePage => typeof(LanguageTimePage),
|
||||
WinoPage.EditAccountDetailsPage => typeof(EditAccountDetailsPage),
|
||||
WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage),
|
||||
WinoPage.ContactsPage => typeof(ContactsPage),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using Wino.Core.WinUI;
|
||||
using Wino.Mail.ViewModels;
|
||||
|
||||
namespace Wino.Views.Abstract;
|
||||
|
||||
public abstract class ContactsPageAbstract : BasePage<ContactsPageViewModel> { }
|
||||
@@ -0,0 +1,241 @@
|
||||
<abstract:ContactsPageAbstract
|
||||
x:Class="Wino.Views.Settings.ContactsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Views.Abstract"
|
||||
xmlns:controls="using:Wino.Controls"
|
||||
xmlns:controls1="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:entities="using:Wino.Core.Domain.Entities.Shared"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
x:Name="root"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<DataTemplate x:Key="ContactTemplate" x:DataType="entities:AccountContact">
|
||||
<Grid Margin="0,4" Padding="16,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Contact Picture -->
|
||||
<PersonPicture
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,16,0"
|
||||
DisplayName="{x:Bind Name}"
|
||||
ProfilePicture="{x:Bind helpers:XamlHelpers.Base64ToBitmapImage(Base64ContactPicture), Mode=OneWay}" />
|
||||
|
||||
<!-- Contact Info -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Address}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border
|
||||
Padding="4,2"
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="2"
|
||||
Visibility="{x:Bind IsRootContact, Mode=OneWay}">
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
|
||||
Text="{x:Bind domain:Translator.ContactStatus_Account, Mode=OneTime}" />
|
||||
</Border>
|
||||
<Border
|
||||
Padding="4,2"
|
||||
CornerRadius="2"
|
||||
Visibility="{x:Bind IsOverridden, Mode=OneWay}">
|
||||
<TextBlock FontSize="10" Text="{x:Bind domain:Translator.ContactStatus_Modified, Mode=OneTime}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Actions -->
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button
|
||||
Command="{Binding ViewModel.EditContactCommand, ElementName=root}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.ContactAction_Edit, Mode=OneTime}">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
Command="{Binding ViewModel.PickContactPhotoCommand, ElementName=root}"
|
||||
CommandParameter="{Binding}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.ContactAction_ChangePhoto, Mode=OneTime}">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
Command="{Binding ViewModel.DeleteContactCommand, ElementName=root}"
|
||||
CommandParameter="{Binding}"
|
||||
IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(IsRootContact), Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
ToolTipService.ToolTip="{x:Bind domain:Translator.ContactAction_Delete, Mode=OneTime}">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Grid Grid.Row="0" Padding="24,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock
|
||||
FontSize="28"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind domain:Translator.ContactsPage_Title, Mode=OneTime}" />
|
||||
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind domain:Translator.ContactsPage_Subtitle, Mode=OneTime}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button Command="{x:Bind ViewModel.AddContactCommand}" Style="{StaticResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.ContactAction_Add, Mode=OneTime}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{x:Bind ViewModel.ToggleSelectionCommand}" Style="{StaticResource DefaultButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock Text="{x:Bind helpers:XamlHelpers.BoolToSelectionModeText(ViewModel.IsSelectionMode), Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Search and Selection Bar -->
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Padding="24,0,24,16"
|
||||
Visibility="{x:Bind ViewModel.IsSelectionMode, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind ViewModel.SelectedContactsCount, Mode=OneWay}"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="{x:Bind ViewModel.SelectedContactsCount, Mode=OneWay}" />
|
||||
<Run Text="{x:Bind domain:Translator.ContactSelection_Selected, Mode=OneTime}" />
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button Command="{x:Bind ViewModel.SelectAllContactsCommand}" Style="{StaticResource SubtleButtonStyle}">
|
||||
<TextBlock Text="{x:Bind domain:Translator.ContactSelection_SelectAll, Mode=OneTime}" />
|
||||
</Button>
|
||||
<Button Command="{x:Bind ViewModel.ClearSelectionCommand}" Style="{StaticResource SubtleButtonStyle}">
|
||||
<TextBlock Text="{x:Bind domain:Translator.ContactSelection_Clear, Mode=OneTime}" />
|
||||
</Button>
|
||||
<Button
|
||||
Command="{x:Bind ViewModel.DeleteSelectedContactsCommand}"
|
||||
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.SelectedContactsCount), Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.ContactAction_Delete, Mode=OneTime}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Search Box -->
|
||||
<AutoSuggestBox
|
||||
Grid.Row="1"
|
||||
Margin="24,0,24,16"
|
||||
PlaceholderText="{x:Bind domain:Translator.ContactsPage_SearchPlaceholder, Mode=OneTime}"
|
||||
QueryIcon="Find"
|
||||
Text="{x:Bind ViewModel.SearchQuery, Mode=TwoWay}"
|
||||
Visibility="{x:Bind ViewModel.IsSelectionMode, Mode=OneWay}" />
|
||||
|
||||
<!-- Content -->
|
||||
<Grid Grid.Row="3" Padding="24,0">
|
||||
<!-- Loading Indicator -->
|
||||
<ProgressRing
|
||||
Width="48"
|
||||
Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="{x:Bind ViewModel.IsLoading, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay}" />
|
||||
|
||||
<!-- Contacts List -->
|
||||
<ListView
|
||||
ItemTemplate="{StaticResource ContactTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.Contacts, Mode=OneWay}"
|
||||
SelectionMode="{x:Bind helpers:XamlHelpers.BoolToSelectionMode(ViewModel.IsSelectionMode), Mode=OneWay}">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection>
|
||||
<AddDeleteThemeTransition />
|
||||
<ContentThemeTransition />
|
||||
<ReorderThemeTransition />
|
||||
<EntranceThemeTransition IsStaggeringEnabled="True" />
|
||||
</TransitionCollection>
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
|
||||
<!-- Empty State -->
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="16"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.CountToVisibilityConverter(ViewModel.SelectedContactsCount), Mode=OneWay}">
|
||||
<FontIcon
|
||||
FontSize="48"
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind domain:Translator.ContactsPage_EmptyState, Mode=OneTime}"
|
||||
TextAlignment="Center" />
|
||||
<Button Command="{x:Bind ViewModel.AddContactCommand}" Style="{StaticResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock Text="{x:Bind domain:Translator.ContactsPage_AddFirstContact, Mode=OneTime}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</abstract:ContactsPageAbstract>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Wino.Views.Abstract;
|
||||
|
||||
namespace Wino.Views.Settings;
|
||||
|
||||
public sealed partial class ContactsPage : ContactsPageAbstract
|
||||
{
|
||||
public ContactsPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\ListView\WinoListViewStyles.xaml" />
|
||||
<None Remove="ShellWindow.xaml" />
|
||||
<None Remove="Views\Settings\ContactsPage.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -124,6 +125,11 @@
|
||||
<ProjectReference Include="..\Wino.Mail.ViewModels\Wino.Mail.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Views\Settings\ContactsPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ListView\WinoListViewStyles.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -59,7 +59,7 @@ public class ContactService : BaseDatabaseService, IContactService
|
||||
{
|
||||
await Connection.InsertAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
else if (!currentContact.IsRootContact) // Don't update root contacts. They belong to accounts.
|
||||
else if (!currentContact.IsRootContact && !currentContact.IsOverridden) // Don't update root contacts or overridden contacts.
|
||||
{
|
||||
await Connection.InsertOrReplaceAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
@@ -70,4 +70,51 @@ public class ContactService : BaseDatabaseService, IContactService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<AccountContact>> GetAllContactsAsync()
|
||||
{
|
||||
return Connection.Table<AccountContact>().ToListAsync();
|
||||
}
|
||||
|
||||
public Task<List<AccountContact>> SearchContactsAsync(string searchQuery)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchQuery))
|
||||
return GetAllContactsAsync();
|
||||
|
||||
var query = new Query(nameof(AccountContact));
|
||||
query.WhereContains("Address", searchQuery.Trim());
|
||||
query.OrWhereContains("Name", searchQuery.Trim());
|
||||
|
||||
var rawLikeQuery = query.GetRawQuery();
|
||||
|
||||
return Connection.QueryAsync<AccountContact>(rawLikeQuery);
|
||||
}
|
||||
|
||||
public async Task<AccountContact> UpdateContactAsync(AccountContact contact)
|
||||
{
|
||||
// Mark the contact as overridden when manually updated
|
||||
contact.IsOverridden = true;
|
||||
|
||||
await Connection.UpdateAsync(contact).ConfigureAwait(false);
|
||||
|
||||
return contact;
|
||||
}
|
||||
|
||||
public async Task DeleteContactAsync(string address)
|
||||
{
|
||||
var contact = await GetAddressInformationByAddressAsync(address).ConfigureAwait(false);
|
||||
|
||||
if (contact != null && !contact.IsRootContact)
|
||||
{
|
||||
await Connection.DeleteAsync(contact).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteContactsAsync(IEnumerable<string> addresses)
|
||||
{
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
await DeleteContactAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user