Entry page is now a Hub

EntryDetailVM and GroupDetailVM are now singleton
Read-only Additional fields and Attachments
This commit is contained in:
Geoffroy BONNEVILLE
2020-05-06 18:54:39 +02:00
parent 1488c3244f
commit ca04a6c8ee
15 changed files with 369 additions and 122 deletions

View File

@@ -29,6 +29,7 @@ namespace ModernKeePass.Application.Entry.Models
public bool HasExpirationDate { get; set; } public bool HasExpirationDate { get; set; }
public DateTimeOffset ExpirationDate { get; set; } public DateTimeOffset ExpirationDate { get; set; }
public DateTimeOffset ModificationDate { get; set; } public DateTimeOffset ModificationDate { get; set; }
public Dictionary<string, byte[]> Attachments { get; set; }
public override string ToString() 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.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.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.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));
} }
} }
} }

View File

@@ -76,7 +76,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Common\Constants.cs" /> <Compile Include="Common\Constants.cs" />
<Compile Include="Dtos\Attachment.cs" />
<Compile Include="Dtos\Credentials.cs" /> <Compile Include="Dtos\Credentials.cs" />
<Compile Include="Dtos\Field.cs" />
<Compile Include="Dtos\FileInfo.cs" /> <Compile Include="Dtos\FileInfo.cs" />
<Compile Include="Dtos\PasswordGenerationOptions.cs" /> <Compile Include="Dtos\PasswordGenerationOptions.cs" />
<Compile Include="Entities\BaseEntity.cs" /> <Compile Include="Entities\BaseEntity.cs" />

View File

@@ -0,0 +1,8 @@
namespace ModernKeePass.Domain.Dtos
{
public class Attachment
{
public string Name { get; set; }
public byte[] Content { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace ModernKeePass.Domain.Dtos
{
public class Field
{
public string Name { get; set; }
public string Value { get; set; }
}
}

View File

@@ -18,5 +18,6 @@ namespace ModernKeePass.Domain.Entities
public Color ForegroundColor { get; set; } public Color ForegroundColor { get; set; }
public Color BackgroundColor { get; set; } public Color BackgroundColor { get; set; }
public bool HasExpirationDate { get; set; } public bool HasExpirationDate { get; set; }
public Dictionary<string, byte[]> Attachments { get; set; } = new Dictionary<string, byte[]>();
} }
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using AutoMapper; using AutoMapper;
using ModernKeePass.Domain.Entities; using ModernKeePass.Domain.Entities;
@@ -27,7 +28,8 @@ namespace ModernKeePass.Infrastructure.KeePass
.ForMember(dest => dest.AdditionalFields, opt => opt.MapFrom(src => .ForMember(dest => dest.AdditionalFields, opt => opt.MapFrom(src =>
src.Strings.Where(s => !PwDefs.GetStandardFields().Contains(s.Key)) src.Strings.Where(s => !PwDefs.GetStandardFields().Contains(s.Key))
.ToDictionary(s => s.Key, s => GetEntryValue(src, 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<string, byte[]> (b.Key, b.Value.ReadData()) )));
} }
private string GetEntryValue(PwEntry entry, string key) => entry.Strings.GetSafe(key).ReadString(); private string GetEntryValue(PwEntry entry, string key) => entry.Strings.GetSafe(key).ReadString();

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/2010/manifest" xmlns:m2="http://schemas.microsoft.com/appx/2013/manifest"> <Package xmlns="http://schemas.microsoft.com/appx/2010/manifest" xmlns:m2="http://schemas.microsoft.com/appx/2013/manifest">
<Identity Name="wismna.ModernKeePass" Publisher="CN=0719A91A-C322-4EE0-A257-E60733EECF06" Version="1.17.0.12" /> <Identity Name="wismna.ModernKeePass" Publisher="CN=0719A91A-C322-4EE0-A257-E60733EECF06" Version="1.18.0.12" />
<Properties> <Properties>
<DisplayName>ModernKeePass</DisplayName> <DisplayName>ModernKeePass</DisplayName>
<PublisherDisplayName>wismna</PublisherDisplayName> <PublisherDisplayName>wismna</PublisherDisplayName>

View File

@@ -516,4 +516,19 @@
<data name="NewGroupTextBox.ButtonTooltip" xml:space="preserve"> <data name="NewGroupTextBox.ButtonTooltip" xml:space="preserve">
<value>New group name</value> <value>New group name</value>
</data> </data>
<data name="EntryHubAdditional.Header" xml:space="preserve">
<value>Additional</value>
</data>
<data name="EntryHubAttachments.Header" xml:space="preserve">
<value>Attachments</value>
</data>
<data name="EntryHubMain.Header" xml:space="preserve">
<value>Main</value>
</data>
<data name="EntryHubOther.Header" xml:space="preserve">
<value>Other</value>
</data>
<data name="EntryIcon.Text" xml:space="preserve">
<value>Icon</value>
</data>
</root> </root>

View File

@@ -516,4 +516,19 @@
<data name="NewGroupTextBox.ButtonTooltip" xml:space="preserve"> <data name="NewGroupTextBox.ButtonTooltip" xml:space="preserve">
<value>Nom du groupe</value> <value>Nom du groupe</value>
</data> </data>
<data name="EntryHubAdditional.Header" xml:space="preserve">
<value>Additionnel</value>
</data>
<data name="EntryHubAttachments.Header" xml:space="preserve">
<value>Pièce jointes</value>
</data>
<data name="EntryHubMain.Header" xml:space="preserve">
<value>Principal</value>
</data>
<data name="EntryHubOther.Header" xml:space="preserve">
<value>Autres</value>
</data>
<data name="EntryIcon.Text" xml:space="preserve">
<value>Icone</value>
</data>
</root> </root>

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -29,6 +28,7 @@ using ModernKeePass.Application.Security.Queries.EstimatePasswordComplexity;
using ModernKeePass.Domain.Enums; using ModernKeePass.Domain.Enums;
using ModernKeePass.Application.Group.Models; using ModernKeePass.Application.Group.Models;
using ModernKeePass.Common; using ModernKeePass.Common;
using ModernKeePass.Domain.Dtos;
using ModernKeePass.Domain.Exceptions; using ModernKeePass.Domain.Exceptions;
using ModernKeePass.Extensions; using ModernKeePass.Extensions;
using ModernKeePass.Models; using ModernKeePass.Models;
@@ -62,9 +62,10 @@ namespace ModernKeePass.ViewModels
return database.IsRecycleBinEnabled && _parent.Id != database.RecycleBinId; return database.IsRecycleBinEnabled && _parent.Id != database.RecycleBinId;
} }
} }
public IEnumerable<GroupVm> BreadCrumb => new List<GroupVm> { _parent };
public ObservableCollection<EntryVm> History { get; private set; } public ObservableCollection<EntryVm> History { get; private set; }
public ObservableCollection<Field> AdditionalFields { get; private set; }
public ObservableCollection<Attachment> Attachments { get; private set; }
/// <summary> /// <summary>
/// Determines if the Entry is current or from history /// Determines if the Entry is current or from history
@@ -76,10 +77,24 @@ namespace ModernKeePass.ViewModels
get { return _selectedItem; } get { return _selectedItem; }
set set
{ {
Set(() => SelectedItem, ref _selectedItem, value); Set(() => SelectedItem, ref _selectedItem, value, true);
if (value != null) RaisePropertyChanged(); if (value != null)
{
AdditionalFields = new ObservableCollection<Field>(SelectedItem.AdditionalFields.Select(f => new Field
{
Name = f.Key,
Value = f.Value
}));
Attachments = new ObservableCollection<Attachment>(SelectedItem.Attachments.Select(f => new Attachment
{
Name = f.Key,
Content = f.Value
}));
RaisePropertyChanged(string.Empty);
}
} }
} }
public int SelectedIndex public int SelectedIndex
{ {
get { return _selectedIndex; } get { return _selectedIndex; }
@@ -235,6 +250,7 @@ namespace ModernKeePass.ViewModels
public RelayCommand DeleteCommand { get; } public RelayCommand DeleteCommand { get; }
public RelayCommand GoBackCommand { get; } public RelayCommand GoBackCommand { get; }
public RelayCommand GoToParentCommand { get; set; } public RelayCommand GoToParentCommand { get; set; }
public RelayCommand<Attachment> OpenAttachmentCommand { get; set; }
private DatabaseVm Database => _mediator.Send(new GetDatabaseQuery()).GetAwaiter().GetResult(); private DatabaseVm Database => _mediator.Send(new GetDatabaseQuery()).GetAwaiter().GetResult();
@@ -243,6 +259,7 @@ namespace ModernKeePass.ViewModels
private readonly IResourceProxy _resource; private readonly IResourceProxy _resource;
private readonly IDialogService _dialog; private readonly IDialogService _dialog;
private readonly INotificationService _notification; private readonly INotificationService _notification;
private readonly IFileProxy _file;
private GroupVm _parent; private GroupVm _parent;
private EntryVm _selectedItem; private EntryVm _selectedItem;
private int _selectedIndex; private int _selectedIndex;
@@ -251,13 +268,14 @@ namespace ModernKeePass.ViewModels
private double _passwordLength = 25; private double _passwordLength = 25;
private bool _isDirty; 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; _mediator = mediator;
_navigation = navigation; _navigation = navigation;
_resource = resource; _resource = resource;
_dialog = dialog; _dialog = dialog;
_notification = notification; _notification = notification;
_file = file;
SaveCommand = new RelayCommand(async () => await SaveChanges(), () => Database.IsDirty); SaveCommand = new RelayCommand(async () => await SaveChanges(), () => Database.IsDirty);
GeneratePasswordCommand = new RelayCommand(async () => await GeneratePassword()); GeneratePasswordCommand = new RelayCommand(async () => await GeneratePassword());
@@ -266,10 +284,12 @@ namespace ModernKeePass.ViewModels
DeleteCommand = new RelayCommand(async () => await AskForDelete()); DeleteCommand = new RelayCommand(async () => await AskForDelete());
GoBackCommand = new RelayCommand(() => _navigation.GoBack()); GoBackCommand = new RelayCommand(() => _navigation.GoBack());
GoToParentCommand = new RelayCommand(() => GoToGroup(_parent.Id)); GoToParentCommand = new RelayCommand(() => GoToGroup(_parent.Id));
OpenAttachmentCommand = new RelayCommand<Attachment>(async attachment => await OpenAttachment(attachment));
MessengerInstance.Register<DatabaseSavedMessage>(this, _ => SaveCommand.RaiseCanExecuteChanged()); MessengerInstance.Register<DatabaseSavedMessage>(this, _ => SaveCommand.RaiseCanExecuteChanged());
} }
public async Task Initialize(string entryId) public async Task Initialize(string entryId)
{ {
SelectedItem = await _mediator.Send(new GetEntryQuery { Id = 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] }); 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() private async Task RestoreHistory()
{ {
await _mediator.Send(new RestoreHistoryCommand { Entry = History[0], HistoryIndex = History.Count - SelectedIndex - 1 }); await _mediator.Send(new RestoreHistoryCommand { Entry = History[0], HistoryIndex = History.Count - SelectedIndex - 1 });
@@ -389,9 +414,15 @@ namespace ModernKeePass.ViewModels
_navigation.GoBack(); _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);
} }
} }
} }

View File

@@ -37,7 +37,7 @@ namespace ModernKeePass.ViewModels
public MainVm Main => ServiceLocator.Current.GetInstance<MainVm>(Guid.NewGuid().ToString()); public MainVm Main => ServiceLocator.Current.GetInstance<MainVm>(Guid.NewGuid().ToString());
public SettingsVm Settings => ServiceLocator.Current.GetInstance<SettingsVm>(Guid.NewGuid().ToString()); public SettingsVm Settings => ServiceLocator.Current.GetInstance<SettingsVm>(Guid.NewGuid().ToString());
public GroupDetailVm Group => ServiceLocator.Current.GetInstance<GroupDetailVm>(Guid.NewGuid().ToString()); public GroupDetailVm Group => ServiceLocator.Current.GetInstance<GroupDetailVm>();
public EntryDetailVm Entry => ServiceLocator.Current.GetInstance<EntryDetailVm>(Guid.NewGuid().ToString()); public EntryDetailVm Entry => ServiceLocator.Current.GetInstance<EntryDetailVm>();
} }
} }

View File

@@ -380,78 +380,138 @@
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}" /> SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
<Grid Grid.Column="1"> <Grid Grid.Column="1">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <Hub Padding="0">
<StackPanel Margin="20,0,0,20"> <Hub.Resources>
<StackPanel.Resources> <Style TargetType="TextBlock" x:Key="EntryTextBlockStyle">
<Style TargetType="TextBlock"> <Setter Property="Margin" Value="0,20,0,0"/>
<Setter Property="Margin" Value="0,20,0,0"/> <Setter Property="FontSize" Value="16"/>
<Setter Property="FontSize" Value="18"/> <Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="TextWrapping" Value="Wrap"/> </Style>
</Style> </Hub.Resources>
<Style TargetType="CheckBox"> <HubSection x:Uid="EntryHubMain" IsHeaderInteractive="True">
<Setter Property="Margin" Value="0,20,0,0"/> <DataTemplate>
<Setter Property="FontSize" Value="18"/> <ScrollViewer VerticalScrollBarVisibility="Auto">
</Style> <StackPanel Margin="20,0,0,20" MinWidth="400">
</StackPanel.Resources> <StackPanel.Resources>
<TextBlock x:Uid="EntryLogin" /> <Style TargetType="CheckBox">
<local:TextBoxWithButton x:Uid="LoginTextBox" Text="{Binding UserName, Mode=TwoWay}" Style="{StaticResource TextBoxWithButtonStyle}" ButtonSymbol="&#xE16F;" IsEnabled="{Binding IsCurrentEntry}"> <Setter Property="Margin" Value="0,20,0,0"/>
<interactivity:Interaction.Behaviors> <Setter Property="FontSize" Value="18"/>
<core:EventTriggerBehavior EventName="ButtonClick"> </Style>
<actions:ClipboardAction Text="{Binding UserName}" /> </StackPanel.Resources>
<actions:ToastAction x:Uid="ToastCopyLogin" Title="{Binding Title}" /> <TextBlock x:Uid="EntryLogin" Style="{StaticResource EntryTextBlockStyle}" />
</core:EventTriggerBehavior> <local:TextBoxWithButton x:Uid="LoginTextBox" Text="{Binding UserName, Mode=TwoWay}" Style="{StaticResource TextBoxWithButtonStyle}" ButtonSymbol="&#xE16F;" IsEnabled="{Binding IsCurrentEntry}">
</interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
</local:TextBoxWithButton> <core:EventTriggerBehavior EventName="ButtonClick">
<TextBlock x:Uid="EntryPassword" /> <actions:ClipboardAction Text="{Binding UserName}" />
<local:PasswordBoxWithButton Password="{Binding Password, Mode=TwoWay}" IsPasswordRevealEnabled="True" Visibility="{Binding IsRevealPassword, Converter={StaticResource InverseBooleanToVisibilityConverter}}" Style="{StaticResource PasswordBoxWithButtonStyle}" IsEnabled="{Binding IsCurrentEntry}" ButtonSymbol="&#xE15E;" /> <actions:ToastAction x:Uid="ToastCopyLogin" Title="{Binding Title}" />
<local:TextBoxWithButton x:Uid="PasswordTextBox" Text="{Binding Password, Mode=TwoWay}" Visibility="{Binding IsRevealPassword, Converter={StaticResource BooleanToVisibilityConverter}}" Style="{StaticResource TextBoxWithButtonStyle}" ButtonSymbol="&#xE16F;" IsEnabled="{Binding IsCurrentEntry}"> </core:EventTriggerBehavior>
<interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ButtonClick"> </local:TextBoxWithButton>
<actions:ClipboardAction Text="{Binding Password}" /> <TextBlock x:Uid="EntryPassword" Style="{StaticResource EntryTextBlockStyle}" />
<actions:ToastAction x:Uid="ToastCopyPassword" Title="{Binding Title}" /> <local:PasswordBoxWithButton Password="{Binding Password, Mode=TwoWay}" IsPasswordRevealEnabled="True" Visibility="{Binding IsRevealPassword, Converter={StaticResource InverseBooleanToVisibilityConverter}}" Style="{StaticResource PasswordBoxWithButtonStyle}" IsEnabled="{Binding IsCurrentEntry}" ButtonSymbol="&#xE15E;" />
</core:EventTriggerBehavior> <local:TextBoxWithButton x:Uid="PasswordTextBox" Text="{Binding Password, Mode=TwoWay}" Visibility="{Binding IsRevealPassword, Converter={StaticResource BooleanToVisibilityConverter}}" Style="{StaticResource TextBoxWithButtonStyle}" ButtonSymbol="&#xE16F;" IsEnabled="{Binding IsCurrentEntry}">
</interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
</local:TextBoxWithButton> <core:EventTriggerBehavior EventName="ButtonClick">
<ProgressBar Value="{Binding PasswordComplexityIndicator, ConverterParameter=0\,128, Converter={StaticResource ProgressBarLegalValuesConverter}}" Maximum="128" Width="350" HorizontalAlignment="Left" Foreground="{Binding PasswordComplexityIndicator, ConverterParameter=128, Converter={StaticResource DoubleToForegroundBrushComplexityConverter}}" /> <actions:ClipboardAction Text="{Binding Password}" />
<CheckBox x:Uid="EntryShowPassword" HorizontalAlignment="Left" Margin="-3,0,0,0" IsChecked="{Binding IsRevealPassword, Mode=TwoWay}" IsEnabled="{Binding IsRevealPasswordEnabled}" /> <actions:ToastAction x:Uid="ToastCopyPassword" Title="{Binding Title}" />
<TextBlock TextWrapping="Wrap" Text="URL" FontSize="18"/> </core:EventTriggerBehavior>
<local:TextBoxWithButton x:Uid="UrlTextBox" Text="{Binding Url, Mode=TwoWay}" Style="{StaticResource TextBoxWithButtonStyle}" ButtonSymbol="&#xE111;" IsEnabled="{Binding IsCurrentEntry}"> </interactivity:Interaction.Behaviors>
<interactivity:Interaction.Behaviors> </local:TextBoxWithButton>
<core:EventTriggerBehavior EventName="ButtonClick"> <ProgressBar Value="{Binding PasswordComplexityIndicator, ConverterParameter=0\,128, Converter={StaticResource ProgressBarLegalValuesConverter}}" Maximum="128" Width="350" HorizontalAlignment="Left" Foreground="{Binding PasswordComplexityIndicator, ConverterParameter=128, Converter={StaticResource DoubleToForegroundBrushComplexityConverter}}" />
<actions:NavigateToUrlAction Url="{Binding Url}" /> <CheckBox x:Uid="EntryShowPassword" HorizontalAlignment="Left" Margin="-3,0,0,0" IsChecked="{Binding IsRevealPassword, Mode=TwoWay}" IsEnabled="{Binding IsRevealPasswordEnabled}" />
</core:EventTriggerBehavior> <TextBlock Text="URL" Style="{StaticResource EntryTextBlockStyle}"/>
</interactivity:Interaction.Behaviors> <local:TextBoxWithButton x:Uid="UrlTextBox" Text="{Binding Url, Mode=TwoWay}" Style="{StaticResource TextBoxWithButtonStyle}" ButtonSymbol="&#xE111;" IsEnabled="{Binding IsCurrentEntry}">
</local:TextBoxWithButton> <interactivity:Interaction.Behaviors>
<TextBlock x:Uid="EntryNotes" /> <core:EventTriggerBehavior EventName="ButtonClick">
<TextBox HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Notes, Mode=TwoWay}" Width="350" Height="200" AcceptsReturn="True" IsSpellCheckEnabled="True" IsEnabled="{Binding IsCurrentEntry}" /> <actions:NavigateToUrlAction Url="{Binding Url}" />
<CheckBox x:Uid="EntryExpirationDate" IsChecked="{Binding HasExpirationDate, Mode=TwoWay}" IsEnabled="{Binding IsCurrentEntry}" /> </core:EventTriggerBehavior>
<Grid> </interactivity:Interaction.Behaviors>
<Grid.ColumnDefinitions> </local:TextBoxWithButton>
<ColumnDefinition Width="Auto" /> <TextBlock x:Uid="EntryNotes" Style="{StaticResource EntryTextBlockStyle}" />
<ColumnDefinition Width="*" /> <TextBox HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Notes, Mode=TwoWay}" Width="350" Height="200" AcceptsReturn="True" IsSpellCheckEnabled="True" IsEnabled="{Binding IsCurrentEntry}" />
</Grid.ColumnDefinitions> <CheckBox x:Uid="EntryExpirationDate" IsChecked="{Binding HasExpirationDate, Mode=TwoWay}" IsEnabled="{Binding IsCurrentEntry}" />
<SymbolIcon Grid.Column="0" Symbol="Important" Foreground="DarkRed" Visibility="{Binding HasExpired, Converter={StaticResource BooleanToVisibilityConverter}}"> <Grid>
<ToolTipService.ToolTip> <Grid.ColumnDefinitions>
<ToolTip x:Uid="EntryExpirationTooltip" /> <ColumnDefinition Width="Auto" />
</ToolTipService.ToolTip> <ColumnDefinition Width="*" />
</SymbolIcon> </Grid.ColumnDefinitions>
<StackPanel Grid.Column="1" x:Name="ExpirationDatePanel" Orientation="Horizontal" Visibility="{Binding HasExpirationDate, Converter={StaticResource BooleanToVisibilityConverter}}"> <SymbolIcon Grid.Column="0" Symbol="Important" Foreground="DarkRed" Visibility="{Binding HasExpired, Converter={StaticResource BooleanToVisibilityConverter}}">
<DatePicker Margin="0,0,20,0" Date="{Binding ExpiryDate, Mode=TwoWay}" /> <ToolTipService.ToolTip>
<TimePicker Time="{Binding ExpiryTime, Mode=TwoWay}" /> <ToolTip x:Uid="EntryExpirationTooltip" />
</ToolTipService.ToolTip>
</SymbolIcon>
<StackPanel Grid.Column="1" x:Name="ExpirationDatePanel" Orientation="Horizontal" Visibility="{Binding HasExpirationDate, Converter={StaticResource BooleanToVisibilityConverter}}">
<DatePicker Margin="0,0,20,0" Date="{Binding ExpiryDate, Mode=TwoWay}" />
<TimePicker Time="{Binding ExpiryTime, Mode=TwoWay}" />
</StackPanel>
</Grid>
</StackPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Small">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ExpirationDatePanel" Storyboard.TargetProperty="Orientation">
<DiscreteObjectKeyFrame KeyTime="0" Value="Vertical"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Large">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ExpirationDatePanel" Storyboard.TargetProperty="Orientation">
<DiscreteObjectKeyFrame KeyTime="0" Value="Horizontal"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ScrollViewer>
</DataTemplate>
</HubSection>
<HubSection x:Uid="EntryHubAdditional" IsHeaderInteractive="True">
<DataTemplate>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding AdditionalFields}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Name}" Style="{StaticResource EntryTextBlockStyle}" />
<TextBox HorizontalAlignment="Left" Text="{Binding Value, Mode=TwoWay}" Width="350" IsEnabled="{Binding Source={StaticResource Locator}, Path=Entry.IsCurrentEntry}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DataTemplate>
</HubSection>
<HubSection x:Uid="EntryHubOther" IsHeaderInteractive="True">
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="EntryIcon" Style="{StaticResource EntryTextBlockStyle}" />
<userControls:SymbolPickerUserControl SelectedSymbol="{Binding Icon, Mode=TwoWay}" HorizontalAlignment="Left" />
<TextBlock x:Uid="EntryBackgroundColor" Style="{StaticResource EntryTextBlockStyle}" />
<userControls:ColorPickerUserControl SelectedColor="{Binding BackgroundColor, Mode=TwoWay}" IsEnabled="{Binding IsCurrentEntry}" Width="350" />
<TextBlock x:Uid="EntryForegroundColor" Style="{StaticResource EntryTextBlockStyle}" />
<userControls:ColorPickerUserControl SelectedColor="{Binding ForegroundColor, Mode=TwoWay}" IsEnabled="{Binding IsCurrentEntry}" Width="350" />
</StackPanel> </StackPanel>
</Grid> </DataTemplate>
<StackPanel x:Name="EditDesign" Visibility="{Binding IsEditMode, Converter={StaticResource BooleanToVisibilityConverter}}" Orientation="Horizontal"> </HubSection>
<StackPanel Width="250" HorizontalAlignment="Left"> <HubSection x:Uid="EntryHubAttachments" IsHeaderInteractive="True">
<TextBlock x:Uid="EntryBackgroundColor" /> <DataTemplate>
<userControls:ColorPickerUserControl SelectedColor="{Binding BackgroundColor, Mode=TwoWay}" IsEnabled="{Binding IsCurrentEntry}" /> <ScrollViewer VerticalScrollBarVisibility="Auto">
</StackPanel> <ItemsControl ItemsSource="{Binding Attachments}">
<StackPanel Width="250" HorizontalAlignment="Left"> <ItemsControl.ItemTemplate>
<TextBlock x:Uid="EntryForegroundColor" /> <DataTemplate>
<userControls:ColorPickerUserControl SelectedColor="{Binding ForegroundColor, Mode=TwoWay}" IsEnabled="{Binding IsCurrentEntry}" /> <StackPanel Orientation="Vertical">
</StackPanel> <HyperlinkButton Content="{Binding Name}" Command="{Binding Source={StaticResource Locator}, Path=Entry.OpenAttachmentCommand}" CommandParameter="{Binding}" />
</StackPanel> </StackPanel>
</StackPanel> </DataTemplate>
</ScrollViewer> </ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DataTemplate>
</HubSection>
</Hub>
</Grid> </Grid>
</Grid> </Grid>
<!-- Bouton Précédent et titre de la page --> <!-- Bouton Précédent et titre de la page -->
@@ -482,10 +542,7 @@
<ToolTip Content="{Binding ParentGroupName}" /> <ToolTip Content="{Binding ParentGroupName}" />
</ToolTipService.ToolTip> </ToolTipService.ToolTip>
</Button> </Button>
<Viewbox MaxHeight="200" Visibility="{Binding IsEditMode, Converter={StaticResource BooleanToVisibilityConverter}}"> <Viewbox MaxHeight="200">
<userControls:SymbolPickerUserControl Width="100" Height="70" SelectedSymbol="{Binding Icon, Mode=TwoWay}" />
</Viewbox>
<Viewbox MaxHeight="200" Visibility="{Binding IsEditMode, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
<SymbolIcon Symbol="{Binding Icon}" Width="100" Height="70" /> <SymbolIcon Symbol="{Binding Icon}" Width="100" Height="70" />
</Viewbox> </Viewbox>
<TextBox <TextBox
@@ -526,29 +583,5 @@
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
</userControls:TopMenuUserControl> </userControls:TopMenuUserControl>
</Grid> </Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Small">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ExpirationDatePanel" Storyboard.TargetProperty="Orientation">
<DiscreteObjectKeyFrame KeyTime="0" Value="Vertical"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="EditDesign" Storyboard.TargetProperty="Orientation">
<DiscreteObjectKeyFrame KeyTime="0" Value="Vertical"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Large">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ExpirationDatePanel" Storyboard.TargetProperty="Orientation">
<DiscreteObjectKeyFrame KeyTime="0" Value="Horizontal"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="EditDesign" Storyboard.TargetProperty="Orientation">
<DiscreteObjectKeyFrame KeyTime="0" Value="Horizontal"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid> </Grid>
</Page> </Page>

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,140 @@
using System.Collections.Generic;
using System.Linq;
using Windows.Foundation.Collections;
namespace ModernKeePass.Common
{
/// <summary>
/// Implementation of IObservableMap that supports reentrancy for use as a default view
/// model.
/// </summary>
public class ObservableDictionary : IObservableMap<string, object>
{
private class ObservableDictionaryChangedEventArgs : IMapChangedEventArgs<string>
{
public ObservableDictionaryChangedEventArgs(CollectionChange change, string key)
{
CollectionChange = change;
Key = key;
}
public CollectionChange CollectionChange { get; }
public string Key { get; }
}
private readonly Dictionary<string, object> _dictionary = new Dictionary<string, object>();
public event MapChangedEventHandler<string, object> 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<string, object> item)
{
Add(item.Key, item.Value);
}
public void AddRange(IEnumerable<KeyValuePair<string, object>> 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<string, object> 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<string> 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<object> Values => _dictionary.Values;
public bool Contains(KeyValuePair<string, object> item)
{
return _dictionary.Contains(item);
}
public int Count => _dictionary.Count;
public bool IsReadOnly => false;
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return _dictionary.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _dictionary.GetEnumerator();
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
var arraySize = array.Length;
foreach (var pair in _dictionary)
{
if (arrayIndex >= arraySize) break;
array[arrayIndex++] = pair;
}
}
}
}