stub new add/edit page

This commit is contained in:
Kyle Spearrin 2019-05-07 11:25:21 -04:00
parent 8c31c7290c
commit 13267ff83d
6 changed files with 1152 additions and 2 deletions

View File

@ -43,6 +43,9 @@
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
<DependentUpon>AddEditPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
<DependentUpon>ViewPage.xaml</DependentUpon>
</Compile>

View File

@ -0,0 +1,603 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.AddEditPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
x:DataType="pages:AddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:AddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Icon="cogs.png"
Text="{u:I18n Edit}" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:StringHasValueConverter x:Key="stringHasValue" />
<u:IsNotNullConverter x:Key="notNull" />
</ResourceDictionary>
</ContentPage.Resources>
<ScrollView x:Name="_scrollView">
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n ItemInformation}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Name, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Username}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Cipher.Login.Username, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="LoginUsername"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Password}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding Cipher.Login.MaskedPassword, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}" />
<controls:MonoLabel
FormattedText="{Binding ColoredPassword, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowPassword}" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf058;"
Command="{Binding CheckPasswordCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="LoginPassword"
Grid.Row="0"
Grid.Column="3"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row" IsVisible="{Binding ShowTotp}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n VerificationCodeTotp}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Margin="0, 0, 10, 0"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
HorizontalOptions="End"
HorizontalTextAlignment="End"
VerticalOptions="CenterAndExpand"/>
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
</StackLayout>
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n CardholderName}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Card.CardholderName, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}"/>
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Number}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Cipher.Card.Number, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="CardNumber"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Brand}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Card.Brand, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Expiration}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Card.Expiration, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n SecurityCode}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding Cipher.Card.MaskedCode, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}" />
<controls:MonoLabel
Text="{Binding Cipher.Card.Code, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardCode}" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardCodeIcon}"
Command="{Binding ToggleCardCodeCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="CardCode"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}" />
</StackLayout>
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n IdentityName}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.FullName, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Username}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Username, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Company}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Company, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n SSN}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.SSN, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}"/>
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n PassportNumber}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.PassportNumber, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n LicenseNumber}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.LicenseNumber, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Email}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Email, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Phone}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Phone, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}">
<Label
Text="{u:I18n Address}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Address1, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address1, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.Address2, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address2, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.Address3, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address3, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.FullAddressPart2, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.FullAddressPart2, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.Country, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Country, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowIdentityAddress}" />
</StackLayout>
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowUris}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n URIs}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:LoginUriView">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n URI}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
IsVisible="{Binding IsWebsite, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Label
Text="{u:I18n Website}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
IsVisible="{Binding IsWebsite, Mode=OneWay}" />
<Label
Text="{Binding HostnameOrUri, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf045;"
Command="{Binding BindingContext.LaunchUriCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
IsVisible="{Binding CanLaunch, Mode=OneWay}" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding BindingContext.CopyUriCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
</StackLayout>
<StackLayout StyleClass="box"
IsVisible="{Binding Cipher.Notes, Converter={StaticResource stringHasValue}}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n Notes}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
Text="{Binding Cipher.Notes, Mode=OneWay}"
StyleClass="box-value"
LineBreakMode="WordWrap" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n CustomFields}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:AddEditPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:FaLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsBooleanType}"
Margin="0, 5, 0, 0" />
<StackLayout IsVisible="{Binding IsHiddenType}"
Grid.Row="1"
Grid.Column="0">
<controls:MonoLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue}" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding IsHiddenType}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding BindingContext.CopyFieldCommand, Source={x:Reference _page}}"
CommandParameter="{Binding Field}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAttachments}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n Attachments}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:AttachmentView">
<StackLayout Spacing="0" Padding="0">
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
<Label
Text="{Binding FileName, Mode=OneWay}"
StyleClass="box-value"
VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand" />
<Label
Text="{Binding SizeName, Mode=OneWay}"
StyleClass="box-sub-label"
HorizontalTextAlignment="End"
VerticalTextAlignment="Center" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf019;"
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
VerticalOptions="Center" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
</StackLayout>
<StackLayout StyleClass="box-bottom">
<Label FormattedText="{Binding UpdatedText}"
StyleClass="box-footer-label" />
<Label FormattedText="{Binding PasswordUpdatedText}"
StyleClass="box-footer-label"
IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
</Label.GestureRecognizers>
</Label>
<Label FormattedText="{Binding PasswordHistoryText}"
StyleClass="box-footer-label"
IsVisible="{Binding Cipher.HasPasswordHistory}">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@ -0,0 +1,59 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class AddEditPage : BaseContentPage
{
private readonly IBroadcasterService _broadcasterService;
private AddEditPageViewModel _vm;
public AddEditPage(string cipherId)
{
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_vm = BindingContext as AddEditPageViewModel;
_vm.Page = this;
_vm.CipherId = cipherId;
SetActivityIndicator();
}
protected override async void OnAppearing()
{
base.OnAppearing();
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
{
if(message.Command == "syncCompleted")
{
var data = message.Data as Dictionary<string, object>;
if(data.ContainsKey("successfully"))
{
var success = data["successfully"] as bool?;
if(success.HasValue && success.Value)
{
await _vm.LoadAsync();
}
}
}
});
await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync());
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.CleanUp();
}
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
{
if(DoOnce())
{
await Navigation.PushModalAsync(new NavigationPage(new PasswordHistoryPage(_vm.CipherId)));
}
}
}
}

View File

@ -0,0 +1,478 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class AddEditPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService;
private readonly IUserService _userService;
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuditService _auditService;
private CipherView _cipher;
private List<AddEditPageFieldViewModel> _fields;
private bool _canAccessPremium;
private bool _showPassword;
private bool _showCardCode;
private string _totpCode;
private string _totpCodeFormatted;
private string _totpSec;
private bool _totpLow;
private DateTime? _totpInterval = null;
public AddEditPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_userService = ServiceContainer.Resolve<IUserService>("userService");
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
CopyUriCommand = new Command<LoginUriView>(CopyUri);
CopyFieldCommand = new Command<FieldView>(CopyField);
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync);
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
PageTitle = AppResources.ViewItem;
}
public Command CopyCommand { get; set; }
public Command CopyUriCommand { get; set; }
public Command CopyFieldCommand { get; set; }
public Command LaunchUriCommand { get; set; }
public Command TogglePasswordCommand { get; set; }
public Command ToggleCardCodeCommand { get; set; }
public Command CheckPasswordCommand { get; set; }
public Command DownloadAttachmentCommand { get; set; }
public string CipherId { get; set; }
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value,
additionalPropertyNames: new string[]
{
nameof(IsLogin),
nameof(IsIdentity),
nameof(IsCard),
nameof(IsSecureNote),
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowTotp),
nameof(ColoredPassword),
nameof(UpdatedText),
nameof(PasswordUpdatedText),
nameof(PasswordHistoryText),
nameof(ShowIdentityAddress),
});
}
public List<AddEditPageFieldViewModel> Fields
{
get => _fields;
set => SetProperty(ref _fields, value);
}
public bool CanAccessPremium
{
get => _canAccessPremium;
set => SetProperty(ref _canAccessPremium, value);
}
public bool ShowPassword
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon)
});
}
public bool ShowCardCode
{
get => _showCardCode;
set => SetProperty(ref _showCardCode, value,
additionalPropertyNames: new string[]
{
nameof(ShowCardCodeIcon)
});
}
public bool IsLogin => Cipher?.Type == Core.Enums.CipherType.Login;
public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity;
public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card;
public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote;
public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Cipher.Login.Password);
public FormattedString UpdatedText
{
get
{
var fs = new FormattedString();
fs.Spans.Add(new Span
{
Text = string.Format("{0}:", AppResources.DateUpdated),
FontAttributes = FontAttributes.Bold
});
fs.Spans.Add(new Span
{
Text = string.Format(" {0} {1}",
Cipher.RevisionDate.ToShortDateString(),
Cipher.RevisionDate.ToShortTimeString())
});
return fs;
}
}
public FormattedString PasswordUpdatedText
{
get
{
var fs = new FormattedString();
fs.Spans.Add(new Span
{
Text = string.Format("{0}:", AppResources.DatePasswordUpdated),
FontAttributes = FontAttributes.Bold
});
fs.Spans.Add(new Span
{
Text = string.Format(" {0} {1}",
Cipher.PasswordRevisionDisplayDate?.ToShortDateString(),
Cipher.PasswordRevisionDisplayDate?.ToShortTimeString())
});
return fs;
}
}
public FormattedString PasswordHistoryText
{
get
{
var fs = new FormattedString();
fs.Spans.Add(new Span
{
Text = string.Format("{0}:", AppResources.PasswordHistory),
FontAttributes = FontAttributes.Bold
});
fs.Spans.Add(new Span
{
Text = string.Format(" {0}", Cipher.PasswordHistory.Count.ToString()),
TextColor = (Color)Application.Current.Resources["PrimaryColor"]
});
return fs;
}
}
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
public bool ShowIdentityAddress => IsIdentity && (
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
public string ShowPasswordIcon => ShowPassword ? "" : "";
public string ShowCardCodeIcon => ShowCardCode ? "" : "";
public string TotpCodeFormatted
{
get => _totpCodeFormatted;
set => SetProperty(ref _totpCodeFormatted, value,
additionalPropertyNames: new string[]
{
nameof(ShowTotp)
});
}
public string TotpSec
{
get => _totpSec;
set => SetProperty(ref _totpSec, value);
}
public bool TotpLow
{
get => _totpLow;
set
{
SetProperty(ref _totpLow, value);
Page.Resources["textTotp"] = Application.Current.Resources[value ? "text-danger" : "text-default"];
}
}
public async Task LoadAsync()
{
CleanUp();
var cipher = await _cipherService.GetAsync(CipherId);
Cipher = await cipher.DecryptAsync();
CanAccessPremium = await _userService.CanAccessPremiumAsync();
Fields = Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(f)).ToList();
if(Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
(Cipher.OrganizationUseTotp || CanAccessPremium))
{
await TotpUpdateCodeAsync();
var interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
await TotpTickAsync(interval);
_totpInterval = DateTime.UtcNow;
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
if(_totpInterval == null)
{
return false;
}
var task = TotpTickAsync(interval);
return true;
});
}
}
public void CleanUp()
{
_totpInterval = null;
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;
}
public void ToggleCardCode()
{
ShowCardCode = !ShowCardCode;
}
private async Task TotpUpdateCodeAsync()
{
if(Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null)
{
_totpInterval = null;
return;
}
_totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp);
if(_totpCode != null)
{
if(_totpCode.Length > 4)
{
var half = (int)Math.Floor(_totpCode.Length / 2M);
TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half),
_totpCode.Substring(half));
}
else
{
TotpCodeFormatted = _totpCode;
}
}
else
{
TotpCodeFormatted = null;
_totpInterval = null;
}
}
private async Task TotpTickAsync(int intervalSeconds)
{
var epoc = CoreHelpers.EpocUtcNow() / 1000;
var mod = epoc % intervalSeconds;
var totpSec = intervalSeconds - mod;
TotpSec = totpSec.ToString();
TotpLow = totpSec < 7;
if(mod == 0)
{
await TotpUpdateCodeAsync();
}
}
private async void CheckPasswordAsync()
{
if(!(Page as BaseContentPage).DoOnce())
{
return;
}
if(string.IsNullOrWhiteSpace(Cipher.Login?.Password))
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
await _deviceActionService.HideLoadingAsync();
if(matches > 0)
{
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
matches.ToString("N0")));
}
else
{
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
}
}
private async void DownloadAttachmentAsync(AttachmentView attachment)
{
if(!(Page as BaseContentPage).DoOnce())
{
return;
}
if(Cipher.OrganizationId == null && !CanAccessPremium)
{
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
return;
}
if(attachment.FileSize >= 10485760) // 10 MB
{
var confirmed = await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
AppResources.Yes, AppResources.No);
if(!confirmed)
{
return;
}
}
if(!_deviceActionService.CanOpenFile(attachment.FileName))
{
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
try
{
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment, Cipher.OrganizationId);
await _deviceActionService.HideLoadingAsync();
if(data == null)
{
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToDownloadFile);
return;
}
if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName))
{
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
return;
}
}
catch
{
await _deviceActionService.HideLoadingAsync();
}
}
private async void CopyAsync(string id, string text = null)
{
string name = null;
if(id == "LoginUsername")
{
text = Cipher.Login.Username;
name = AppResources.Username;
}
else if(id == "LoginPassword")
{
text = Cipher.Login.Password;
name = AppResources.Password;
}
else if(id == "LoginTotp")
{
text = _totpCode;
name = AppResources.VerificationCodeTotp;
}
else if(id == "LoginUri")
{
name = AppResources.URI;
}
else if(id == "FieldValue")
{
name = AppResources.Value;
}
else if(id == "CardNumber")
{
text = Cipher.Card.Number;
name = AppResources.Number;
}
else if(id == "CardCode")
{
text = Cipher.Card.Code;
name = AppResources.SecurityCode;
}
if(text != null)
{
await _platformUtilsService.CopyToClipboardAsync(text);
if(!string.IsNullOrWhiteSpace(name))
{
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name));
}
}
}
private void CopyUri(LoginUriView uri)
{
CopyAsync("LoginUri", uri.Uri);
}
private void CopyField(FieldView field)
{
CopyAsync("FieldValue", field.Value);
}
private void LaunchUri(LoginUriView uri)
{
if(uri.CanLaunch && (Page as BaseContentPage).DoOnce())
{
_platformUtilsService.LaunchUri(uri.LaunchUri);
}
}
}
public class AddEditPageFieldViewModel : BaseViewModel
{
private FieldView _field;
private bool _showHiddenValue;
public AddEditPageFieldViewModel(FieldView field)
{
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value,
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(ShowCopyButton),
});
}
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value,
additionalPropertyNames: new string[]
{
nameof(ShowHiddenValueIcon)
});
}
public Command ToggleHiddenValueCommand { get; set; }
public string ValueText => IsBooleanType ? (_field.Value == "true" ? "" : "") : _field.Value;
public string ShowHiddenValueIcon => _showHiddenValue ? "" : "";
public bool IsTextType => _field.Type == Core.Enums.FieldType.Text;
public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean;
public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden;
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
!string.IsNullOrWhiteSpace(_field.Value);
public void ToggleHiddenValue()
{
ShowHiddenValue = !ShowHiddenValue;
}
}
}

View File

@ -15,8 +15,7 @@
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Icon="cogs.png"
Text="{u:I18n Edit}" />
<ToolbarItem Text="{u:I18n Edit}" Clicked="EditToolbarItem_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>

View File

@ -55,5 +55,13 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(new PasswordHistoryPage(_vm.CipherId)));
}
}
private async void EditToolbarItem_Clicked(object sender, System.EventArgs e)
{
if(DoOnce())
{
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
}
}
}
}