From ca04a6c8eede9af19829fbd4e954bfb8ccfae178 Mon Sep 17 00:00:00 2001 From: Geoffroy BONNEVILLE Date: Wed, 6 May 2020 18:54:39 +0200 Subject: [PATCH] Entry page is now a Hub EntryDetailVM and GroupDetailVM are now singleton Read-only Additional fields and Attachments --- .../Entry/Models/EntryVm.cs | 4 +- ModernKeePass.Domain/Domain.csproj | 2 + ModernKeePass.Domain/Dtos/Attachment.cs | 8 + ModernKeePass.Domain/Dtos/Field.cs | 8 + ModernKeePass.Domain/Entities/EntryEntity.cs | 1 + .../KeePass/EntryMappingProfile.cs | 4 +- ModernKeePass/Package.appxmanifest | 2 +- ModernKeePass/Strings/en-US/Resources.resw | 15 ++ ModernKeePass/Strings/fr-FR/Resources.resw | 15 ++ ModernKeePass/ViewModels/EntryDetailVm.cs | 47 +++- ModernKeePass/ViewModels/ViewModelLocator.cs | 4 +- ModernKeePass/Views/EntryDetailPage.xaml | 231 ++++++++++-------- .../en-us/baselisting/releaseNotes.txt | 5 - .../fr-fr/baselisting/releaseNotes.txt | 5 - WinAppCommon/Common/ObservableDictionary.cs | 140 +++++++++++ 15 files changed, 369 insertions(+), 122 deletions(-) create mode 100644 ModernKeePass.Domain/Dtos/Attachment.cs create mode 100644 ModernKeePass.Domain/Dtos/Field.cs create mode 100644 WinAppCommon/Common/ObservableDictionary.cs diff --git a/ModernKeePass.Application/Entry/Models/EntryVm.cs b/ModernKeePass.Application/Entry/Models/EntryVm.cs index 5593938..d327f88 100644 --- a/ModernKeePass.Application/Entry/Models/EntryVm.cs +++ b/ModernKeePass.Application/Entry/Models/EntryVm.cs @@ -29,6 +29,7 @@ namespace ModernKeePass.Application.Entry.Models public bool HasExpirationDate { get; set; } public DateTimeOffset ExpirationDate { get; set; } public DateTimeOffset ModificationDate { get; set; } + public Dictionary Attachments { get; set; } public override string ToString() { @@ -53,7 +54,8 @@ namespace ModernKeePass.Application.Entry.Models .ForMember(d => d.ModificationDate, opts => opts.MapFrom(s => s.LastModificationDate)) .ForMember(d => d.Icon, opts => opts.MapFrom(s => s.HasExpirationDate && s.ExpirationDate < DateTimeOffset.Now ? Icon.ReportHacked : s.Icon)) .ForMember(d => d.ForegroundColor, opts => opts.MapFrom(s => s.ForegroundColor)) - .ForMember(d => d.BackgroundColor, opts => opts.MapFrom(s => s.BackgroundColor)); + .ForMember(d => d.BackgroundColor, opts => opts.MapFrom(s => s.BackgroundColor)) + .ForMember(d => d.Attachments, opts => opts.MapFrom(s => s.Attachments)); } } } \ No newline at end of file diff --git a/ModernKeePass.Domain/Domain.csproj b/ModernKeePass.Domain/Domain.csproj index 14eff4a..83a3c0c 100644 --- a/ModernKeePass.Domain/Domain.csproj +++ b/ModernKeePass.Domain/Domain.csproj @@ -76,7 +76,9 @@ + + diff --git a/ModernKeePass.Domain/Dtos/Attachment.cs b/ModernKeePass.Domain/Dtos/Attachment.cs new file mode 100644 index 0000000..3f0d85d --- /dev/null +++ b/ModernKeePass.Domain/Dtos/Attachment.cs @@ -0,0 +1,8 @@ +namespace ModernKeePass.Domain.Dtos +{ + public class Attachment + { + public string Name { get; set; } + public byte[] Content { get; set; } + } +} \ No newline at end of file diff --git a/ModernKeePass.Domain/Dtos/Field.cs b/ModernKeePass.Domain/Dtos/Field.cs new file mode 100644 index 0000000..7dce65a --- /dev/null +++ b/ModernKeePass.Domain/Dtos/Field.cs @@ -0,0 +1,8 @@ +namespace ModernKeePass.Domain.Dtos +{ + public class Field + { + public string Name { get; set; } + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/ModernKeePass.Domain/Entities/EntryEntity.cs b/ModernKeePass.Domain/Entities/EntryEntity.cs index 5031390..088574c 100644 --- a/ModernKeePass.Domain/Entities/EntryEntity.cs +++ b/ModernKeePass.Domain/Entities/EntryEntity.cs @@ -18,5 +18,6 @@ namespace ModernKeePass.Domain.Entities public Color ForegroundColor { get; set; } public Color BackgroundColor { get; set; } public bool HasExpirationDate { get; set; } + public Dictionary Attachments { get; set; } = new Dictionary(); } } \ No newline at end of file diff --git a/ModernKeePass.Infrastructure/KeePass/EntryMappingProfile.cs b/ModernKeePass.Infrastructure/KeePass/EntryMappingProfile.cs index 04a1898..5c99915 100644 --- a/ModernKeePass.Infrastructure/KeePass/EntryMappingProfile.cs +++ b/ModernKeePass.Infrastructure/KeePass/EntryMappingProfile.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using AutoMapper; using ModernKeePass.Domain.Entities; @@ -27,7 +28,8 @@ namespace ModernKeePass.Infrastructure.KeePass .ForMember(dest => dest.AdditionalFields, opt => opt.MapFrom(src => src.Strings.Where(s => !PwDefs.GetStandardFields().Contains(s.Key)) .ToDictionary(s => s.Key, s => GetEntryValue(src, s.Key)))) - .ForMember(dest => dest.LastModificationDate, opt => opt.MapFrom(src => new DateTimeOffset(src.LastModificationTime))); + .ForMember(dest => dest.LastModificationDate, opt => opt.MapFrom(src => new DateTimeOffset(src.LastModificationTime))) + .ForMember(dest => dest.Attachments, opt => opt.MapFrom(src => src.Binaries.Select(b => new KeyValuePair (b.Key, b.Value.ReadData()) ))); } private string GetEntryValue(PwEntry entry, string key) => entry.Strings.GetSafe(key).ReadString(); diff --git a/ModernKeePass/Package.appxmanifest b/ModernKeePass/Package.appxmanifest index 3ee3005..1fd7f8a 100644 --- a/ModernKeePass/Package.appxmanifest +++ b/ModernKeePass/Package.appxmanifest @@ -1,6 +1,6 @@  - + ModernKeePass wismna diff --git a/ModernKeePass/Strings/en-US/Resources.resw b/ModernKeePass/Strings/en-US/Resources.resw index 21c28cd..4de65c4 100644 --- a/ModernKeePass/Strings/en-US/Resources.resw +++ b/ModernKeePass/Strings/en-US/Resources.resw @@ -516,4 +516,19 @@ New group name + + Additional + + + Attachments + + + Main + + + Other + + + Icon + \ No newline at end of file diff --git a/ModernKeePass/Strings/fr-FR/Resources.resw b/ModernKeePass/Strings/fr-FR/Resources.resw index 0b6ddce..e3125ac 100644 --- a/ModernKeePass/Strings/fr-FR/Resources.resw +++ b/ModernKeePass/Strings/fr-FR/Resources.resw @@ -516,4 +516,19 @@ Nom du groupe + + Additionnel + + + Pièce jointes + + + Principal + + + Autres + + + Icone + \ No newline at end of file diff --git a/ModernKeePass/ViewModels/EntryDetailVm.cs b/ModernKeePass/ViewModels/EntryDetailVm.cs index ce51b37..e5dad60 100644 --- a/ModernKeePass/ViewModels/EntryDetailVm.cs +++ b/ModernKeePass/ViewModels/EntryDetailVm.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -29,6 +28,7 @@ using ModernKeePass.Application.Security.Queries.EstimatePasswordComplexity; using ModernKeePass.Domain.Enums; using ModernKeePass.Application.Group.Models; using ModernKeePass.Common; +using ModernKeePass.Domain.Dtos; using ModernKeePass.Domain.Exceptions; using ModernKeePass.Extensions; using ModernKeePass.Models; @@ -62,9 +62,10 @@ namespace ModernKeePass.ViewModels return database.IsRecycleBinEnabled && _parent.Id != database.RecycleBinId; } } - - public IEnumerable BreadCrumb => new List { _parent }; + public ObservableCollection History { get; private set; } + public ObservableCollection AdditionalFields { get; private set; } + public ObservableCollection Attachments { get; private set; } /// /// Determines if the Entry is current or from history @@ -76,10 +77,24 @@ namespace ModernKeePass.ViewModels get { return _selectedItem; } set { - Set(() => SelectedItem, ref _selectedItem, value); - if (value != null) RaisePropertyChanged(); + Set(() => SelectedItem, ref _selectedItem, value, true); + if (value != null) + { + AdditionalFields = new ObservableCollection(SelectedItem.AdditionalFields.Select(f => new Field + { + Name = f.Key, + Value = f.Value + })); + Attachments = new ObservableCollection(SelectedItem.Attachments.Select(f => new Attachment + { + Name = f.Key, + Content = f.Value + })); + RaisePropertyChanged(string.Empty); + } } } + public int SelectedIndex { get { return _selectedIndex; } @@ -235,6 +250,7 @@ namespace ModernKeePass.ViewModels public RelayCommand DeleteCommand { get; } public RelayCommand GoBackCommand { get; } public RelayCommand GoToParentCommand { get; set; } + public RelayCommand OpenAttachmentCommand { get; set; } private DatabaseVm Database => _mediator.Send(new GetDatabaseQuery()).GetAwaiter().GetResult(); @@ -243,6 +259,7 @@ namespace ModernKeePass.ViewModels private readonly IResourceProxy _resource; private readonly IDialogService _dialog; private readonly INotificationService _notification; + private readonly IFileProxy _file; private GroupVm _parent; private EntryVm _selectedItem; private int _selectedIndex; @@ -251,13 +268,14 @@ namespace ModernKeePass.ViewModels private double _passwordLength = 25; private bool _isDirty; - public EntryDetailVm(IMediator mediator, INavigationService navigation, IResourceProxy resource, IDialogService dialog, INotificationService notification) + public EntryDetailVm(IMediator mediator, INavigationService navigation, IResourceProxy resource, IDialogService dialog, INotificationService notification, IFileProxy file) { _mediator = mediator; _navigation = navigation; _resource = resource; _dialog = dialog; _notification = notification; + _file = file; SaveCommand = new RelayCommand(async () => await SaveChanges(), () => Database.IsDirty); GeneratePasswordCommand = new RelayCommand(async () => await GeneratePassword()); @@ -266,10 +284,12 @@ namespace ModernKeePass.ViewModels DeleteCommand = new RelayCommand(async () => await AskForDelete()); GoBackCommand = new RelayCommand(() => _navigation.GoBack()); GoToParentCommand = new RelayCommand(() => GoToGroup(_parent.Id)); + OpenAttachmentCommand = new RelayCommand(async attachment => await OpenAttachment(attachment)); MessengerInstance.Register(this, _ => SaveCommand.RaiseCanExecuteChanged()); } + public async Task Initialize(string entryId) { SelectedItem = await _mediator.Send(new GetEntryQuery { Id = entryId }); @@ -355,6 +375,11 @@ namespace ModernKeePass.ViewModels if (_isDirty) await _mediator.Send(new AddHistoryCommand { Entry = History[0] }); } + public void GoToGroup(string groupId) + { + _navigation.NavigateTo(Constants.Navigation.GroupPage, new NavigationItem { Id = groupId }); + } + private async Task RestoreHistory() { await _mediator.Send(new RestoreHistoryCommand { Entry = History[0], HistoryIndex = History.Count - SelectedIndex - 1 }); @@ -389,9 +414,15 @@ namespace ModernKeePass.ViewModels _navigation.GoBack(); } - public void GoToGroup(string groupId) + private async Task OpenAttachment(Attachment attachment) { - _navigation.NavigateTo(Constants.Navigation.GroupPage, new NavigationItem { Id = groupId }); + var extensionIndex = attachment.Name.LastIndexOf('.'); + var fileInfo = await _file.CreateFile(attachment.Name, + attachment.Name.Substring(extensionIndex, attachment.Name.Length - extensionIndex), + string.Empty, + false); + if (fileInfo == null) return; + await _file.WriteBinaryContentsToFile(fileInfo.Id, attachment.Content); } } } diff --git a/ModernKeePass/ViewModels/ViewModelLocator.cs b/ModernKeePass/ViewModels/ViewModelLocator.cs index f5959ca..135b62c 100644 --- a/ModernKeePass/ViewModels/ViewModelLocator.cs +++ b/ModernKeePass/ViewModels/ViewModelLocator.cs @@ -37,7 +37,7 @@ namespace ModernKeePass.ViewModels public MainVm Main => ServiceLocator.Current.GetInstance(Guid.NewGuid().ToString()); public SettingsVm Settings => ServiceLocator.Current.GetInstance(Guid.NewGuid().ToString()); - public GroupDetailVm Group => ServiceLocator.Current.GetInstance(Guid.NewGuid().ToString()); - public EntryDetailVm Entry => ServiceLocator.Current.GetInstance(Guid.NewGuid().ToString()); + public GroupDetailVm Group => ServiceLocator.Current.GetInstance(); + public EntryDetailVm Entry => ServiceLocator.Current.GetInstance(); } } \ No newline at end of file diff --git a/ModernKeePass/Views/EntryDetailPage.xaml b/ModernKeePass/Views/EntryDetailPage.xaml index 8f7bc0d..ebfe659 100644 --- a/ModernKeePass/Views/EntryDetailPage.xaml +++ b/ModernKeePass/Views/EntryDetailPage.xaml @@ -380,78 +380,138 @@ SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + @@ -482,10 +542,7 @@ - - - - + - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ModernKeePass/appMetadata/en-us/baselisting/releaseNotes.txt b/ModernKeePass/appMetadata/en-us/baselisting/releaseNotes.txt index a45a8a6..e69de29 100644 --- a/ModernKeePass/appMetadata/en-us/baselisting/releaseNotes.txt +++ b/ModernKeePass/appMetadata/en-us/baselisting/releaseNotes.txt @@ -1,5 +0,0 @@ -Redesign of the UI (top menu and hamburger menu) -Resuming the app re-opens previously opened database -Notify user and show the Save As dialog when an error is encountered during saving -Save As actually works now -Creating a new group is now done inline \ No newline at end of file diff --git a/ModernKeePass/appMetadata/fr-fr/baselisting/releaseNotes.txt b/ModernKeePass/appMetadata/fr-fr/baselisting/releaseNotes.txt index c0a0a74..e69de29 100644 --- a/ModernKeePass/appMetadata/fr-fr/baselisting/releaseNotes.txt +++ b/ModernKeePass/appMetadata/fr-fr/baselisting/releaseNotes.txt @@ -1,5 +0,0 @@ -Redesign de l'interface utilisateur (menu superieur et menu hamburger) -Le re-chargement de l'app re-ouvre la base de donnees ouverte precedemment -L'utlisateur est prevenu et un popup de sauvegarde est affiche quand il y a une erreur de sauvegarde -La fonctionnalite Sauvegarder Sous fonctionne correctement -La creation d'un nouveau groupe se fait directement dans le menu \ No newline at end of file diff --git a/WinAppCommon/Common/ObservableDictionary.cs b/WinAppCommon/Common/ObservableDictionary.cs new file mode 100644 index 0000000..a767e8e --- /dev/null +++ b/WinAppCommon/Common/ObservableDictionary.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Linq; +using Windows.Foundation.Collections; + +namespace ModernKeePass.Common +{ + /// + /// Implementation of IObservableMap that supports reentrancy for use as a default view + /// model. + /// + public class ObservableDictionary : IObservableMap + { + private class ObservableDictionaryChangedEventArgs : IMapChangedEventArgs + { + public ObservableDictionaryChangedEventArgs(CollectionChange change, string key) + { + CollectionChange = change; + Key = key; + } + + public CollectionChange CollectionChange { get; } + public string Key { get; } + } + + private readonly Dictionary _dictionary = new Dictionary(); + public event MapChangedEventHandler MapChanged; + + private void InvokeMapChanged(CollectionChange change, string key) + { + MapChanged?.Invoke(this, new ObservableDictionaryChangedEventArgs(change, key)); + } + + public void Add(string key, object value) + { + _dictionary.Add(key, value); + InvokeMapChanged(CollectionChange.ItemInserted, key); + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void AddRange(IEnumerable> values) + { + foreach (var value in values) + { + Add(value); + } + } + + public bool Remove(string key) + { + if (_dictionary.Remove(key)) + { + InvokeMapChanged(CollectionChange.ItemRemoved, key); + return true; + } + return false; + } + + public bool Remove(KeyValuePair item) + { + object currentValue; + if (_dictionary.TryGetValue(item.Key, out currentValue) && + Equals(item.Value, currentValue) && _dictionary.Remove(item.Key)) + { + InvokeMapChanged(CollectionChange.ItemRemoved, item.Key); + return true; + } + return false; + } + + public object this[string key] + { + get + { + return _dictionary[key]; + } + set + { + _dictionary[key] = value; + InvokeMapChanged(CollectionChange.ItemChanged, key); + } + } + + public void Clear() + { + var priorKeys = _dictionary.Keys.ToArray(); + _dictionary.Clear(); + foreach (var key in priorKeys) + { + InvokeMapChanged(CollectionChange.ItemRemoved, key); + } + } + + public ICollection Keys => _dictionary.Keys; + + public bool ContainsKey(string key) + { + return _dictionary.ContainsKey(key); + } + + public bool TryGetValue(string key, out object value) + { + return _dictionary.TryGetValue(key, out value); + } + + public ICollection Values => _dictionary.Values; + + public bool Contains(KeyValuePair item) + { + return _dictionary.Contains(item); + } + + public int Count => _dictionary.Count; + + public bool IsReadOnly => false; + + public IEnumerator> GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + var arraySize = array.Length; + foreach (var pair in _dictionary) + { + if (arrayIndex >= arraySize) break; + array[arrayIndex++] = pair; + } + } + } +}