diff --git a/src/App/App.csproj b/src/App/App.csproj index f2dde2180..9c88afb94 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -63,6 +63,7 @@ + @@ -98,7 +99,7 @@ - + diff --git a/src/App/Models/View/VaultView.cs b/src/App/Models/Page/VaultListPageModel.cs similarity index 89% rename from src/App/Models/View/VaultView.cs rename to src/App/Models/Page/VaultListPageModel.cs index 1c334c71b..4e8092c18 100644 --- a/src/App/Models/View/VaultView.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using Bit.App.Resources; -namespace Bit.App.Models.View +namespace Bit.App.Models.Page { - public class VaultView + public class VaultListPageModel { public class Site { @@ -44,7 +45,7 @@ namespace Bit.App.Models.View } public string Id { get; set; } - public string Name { get; set; } = "(none)"; + public string Name { get; set; } = AppResources.FolderNone; public string FirstLetter { get { return Name.Substring(0, 1); } } } } diff --git a/src/App/Models/Page/VaultViewSitePageModel.cs b/src/App/Models/Page/VaultViewSitePageModel.cs new file mode 100644 index 000000000..130ffef5a --- /dev/null +++ b/src/App/Models/Page/VaultViewSitePageModel.cs @@ -0,0 +1,93 @@ +using System; +using System.ComponentModel; +using Bit.App.Resources; + +namespace Bit.App.Models.Page +{ + public class VaultViewSitePageModel : INotifyPropertyChanged + { + private string _name; + private string _username; + private string _password; + private string _uri; + private string _notes; + private bool _showPassword; + + public VaultViewSitePageModel() { } + + public event PropertyChangedEventHandler PropertyChanged; + + public string Name + { + get { return _name; } + set + { + _name = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Name))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(PageTitle))); + } + } + public string Username + { + get { return _username; } + set + { + _username = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Username))); + } + } + public string Password + { + get { return _password; } + set + { + _password = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Password))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(MaskedPassword))); + } + } + public string Uri + { + get { return _uri; } + set + { + _uri = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Uri))); + } + } + public string Notes + { + get { return _notes; } + set + { + _notes = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(Notes))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowNotes))); + } + } + public string PageTitle => Name ?? AppResources.SiteNoName; + public bool ShowPassword + { + get { return _showPassword; } + set + { + _showPassword = value; + PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowPassword))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(MaskedPassword))); + PropertyChanged(this, new PropertyChangedEventArgs(nameof(ShowHideText))); + } + } + public string MaskedPassword => ShowPassword ? Password : Password == null ? null : new string('●', Password.Length); + public string ShowHideText => ShowPassword ? AppResources.Hide : AppResources.Show; + public bool ShowNotes => !string.IsNullOrWhiteSpace(Notes); + + public void Update(Site site) + { + Name = site.Name?.Decrypt(); + Username = site.Username?.Decrypt(); + Password = site.Password?.Decrypt(); + Uri = site.Uri?.Decrypt(); + Notes = site.Notes?.Decrypt(); + } + } +} diff --git a/src/App/Pages/VaultAddSitePage.cs b/src/App/Pages/VaultAddSitePage.cs index f6af6c801..0bb4c1cdf 100644 --- a/src/App/Pages/VaultAddSitePage.cs +++ b/src/App/Pages/VaultAddSitePage.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Reflection.Emit; -using System.Text; using Acr.UserDialogs; using Bit.App.Abstractions; using Bit.App.Models; @@ -15,19 +12,26 @@ namespace Bit.App.Pages { public class VaultAddSitePage : ContentPage { + private readonly ISiteService _siteService; + private readonly IFolderService _folderService; + private readonly IUserDialogs _userDialogs; + private readonly IConnectivity _connectivity; + public VaultAddSitePage() { - var cryptoService = Resolver.Resolve(); - var siteService = Resolver.Resolve(); - var folderService = Resolver.Resolve(); - var userDialogs = Resolver.Resolve(); - var connectivity = Resolver.Resolve(); + _siteService = Resolver.Resolve(); + _folderService = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); + _connectivity = Resolver.Resolve(); + } - var folders = folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); + private void Init() + { + var folders = _folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); var uriEntry = new Entry { Keyboard = Keyboard.Url }; var nameEntry = new Entry(); - var folderPicker = new Picker { Title = "Folder" }; + var folderPicker = new Picker { Title = AppResources.Folder }; folderPicker.Items.Add(AppResources.FolderNone); folderPicker.SelectedIndex = 0; foreach(var folder in folders) @@ -60,7 +64,7 @@ namespace Bit.App.Pages var saveToolBarItem = new ToolbarItem(AppResources.Save, null, async () => { - if(!connectivity.IsConnected) + if(!_connectivity.IsConnected) { AlertNoConnection(); return; @@ -92,26 +96,26 @@ namespace Bit.App.Pages site.FolderId = folders.ElementAt(folderPicker.SelectedIndex - 1).Id; } - var saveTask = siteService.SaveAsync(site); - userDialogs.ShowLoading("Saving...", MaskType.Black); + var saveTask = _siteService.SaveAsync(site); + _userDialogs.ShowLoading("Saving...", MaskType.Black); await saveTask; - userDialogs.HideLoading(); + _userDialogs.HideLoading(); await Navigation.PopAsync(); - userDialogs.SuccessToast(nameEntry.Text, "New site created."); + _userDialogs.SuccessToast(nameEntry.Text, "New site created."); }, ToolbarItemOrder.Default, 0); Title = AppResources.AddSite; Content = scrollView; ToolbarItems.Add(saveToolBarItem); - if(!connectivity.IsConnected) + if(!_connectivity.IsConnected) { AlertNoConnection(); } } - public void AlertNoConnection() + private void AlertNoConnection() { DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, AppResources.Ok); } diff --git a/src/App/Pages/VaultEditSitePage.cs b/src/App/Pages/VaultEditSitePage.cs index b9758567e..1a690f645 100644 --- a/src/App/Pages/VaultEditSitePage.cs +++ b/src/App/Pages/VaultEditSitePage.cs @@ -3,17 +3,139 @@ using System.Collections.Generic; using System.Linq; using System.Reflection.Emit; using System.Text; - +using Acr.UserDialogs; +using Bit.App.Abstractions; +using Bit.App.Resources; +using Plugin.Connectivity.Abstractions; using Xamarin.Forms; +using XLabs.Ioc; namespace Bit.App.Pages { public class VaultEditSitePage : ContentPage { + private readonly string _siteId; + private readonly ISiteService _siteService; + private readonly IFolderService _folderService; + private readonly IUserDialogs _userDialogs; + private readonly IConnectivity _connectivity; + public VaultEditSitePage(string siteId) { - Title = "Edit Site " + siteId; - Content = null; + _siteId = siteId; + _siteService = Resolver.Resolve(); + _folderService = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); + _connectivity = Resolver.Resolve(); + + Init(); + } + + private void Init() + { + var site = _siteService.GetByIdAsync(_siteId).GetAwaiter().GetResult(); + if(site == null) + { + // TODO: handle error. navigate back? should never happen... + return; + } + + var folders = _folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); + + var uriEntry = new Entry { Keyboard = Keyboard.Url, Text = site.Uri?.Decrypt() }; + var nameEntry = new Entry { Text = site.Name?.Decrypt() }; + var folderPicker = new Picker { Title = AppResources.Folder }; + folderPicker.Items.Add(AppResources.FolderNone); + int selectedIndex = 0; + int i = 0; + foreach(var folder in folders) + { + i++; + if(folder.Id == site.FolderId) + { + selectedIndex = i; + } + + folderPicker.Items.Add(folder.Name.Decrypt()); + } + folderPicker.SelectedIndex = selectedIndex; + var usernameEntry = new Entry { Text = site.Username?.Decrypt() }; + var passwordEntry = new Entry { IsPassword = true, Text = site.Password?.Decrypt() }; + var notesEditor = new Editor { Text = site.Notes?.Decrypt() }; + + var stackLayout = new StackLayout(); + stackLayout.Children.Add(new Label { Text = AppResources.URI }); + stackLayout.Children.Add(uriEntry); + stackLayout.Children.Add(new Label { Text = AppResources.Name }); + stackLayout.Children.Add(nameEntry); + stackLayout.Children.Add(new Label { Text = AppResources.Folder }); + stackLayout.Children.Add(folderPicker); + stackLayout.Children.Add(new Label { Text = AppResources.Username }); + stackLayout.Children.Add(usernameEntry); + stackLayout.Children.Add(new Label { Text = AppResources.Password }); + stackLayout.Children.Add(passwordEntry); + stackLayout.Children.Add(new Label { Text = AppResources.Notes }); + stackLayout.Children.Add(notesEditor); + + var scrollView = new ScrollView + { + Content = stackLayout, + Orientation = ScrollOrientation.Vertical + }; + + var saveToolBarItem = new ToolbarItem(AppResources.Save, null, async () => + { + if(!_connectivity.IsConnected) + { + AlertNoConnection(); + return; + } + + if(string.IsNullOrWhiteSpace(uriEntry.Text)) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.URI), AppResources.Ok); + return; + } + + if(string.IsNullOrWhiteSpace(nameEntry.Text)) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.Name), AppResources.Ok); + return; + } + + site.Uri = uriEntry.Text.Encrypt(); + site.Name = nameEntry.Text.Encrypt(); + site.Username = usernameEntry.Text?.Encrypt(); + site.Password = passwordEntry.Text?.Encrypt(); + site.Notes = notesEditor.Text?.Encrypt(); + + if(folderPicker.SelectedIndex > 0) + { + site.FolderId = folders.ElementAt(folderPicker.SelectedIndex - 1).Id; + } + + var saveTask = _siteService.SaveAsync(site); + _userDialogs.ShowLoading("Saving...", MaskType.Black); + await saveTask; + + _userDialogs.HideLoading(); + await Navigation.PopAsync(); + _userDialogs.SuccessToast(nameEntry.Text, "Site updated."); + }, ToolbarItemOrder.Default, 0); + + Title = "Edit Site"; + Content = scrollView; + ToolbarItems.Add(saveToolBarItem); + + if(!_connectivity.IsConnected) + { + AlertNoConnection(); + } + } + + private void AlertNoConnection() + { + DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, AppResources.Ok); } } } diff --git a/src/App/Pages/VaultListPage.cs b/src/App/Pages/VaultListPage.cs index c0011a94d..661dae3d1 100644 --- a/src/App/Pages/VaultListPage.cs +++ b/src/App/Pages/VaultListPage.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Acr.UserDialogs; using Bit.App.Abstractions; -using Bit.App.Models.View; +using Bit.App.Models.Page; using Bit.App.Resources; using Xamarin.Forms; using XLabs.Ioc; @@ -28,7 +28,7 @@ namespace Bit.App.Pages Init(); } - public ObservableCollection Folders { get; private set; } = new ObservableCollection(); + public ObservableCollection Folders { get; private set; } = new ObservableCollection(); private void Init() { @@ -41,7 +41,7 @@ namespace Bit.App.Pages Title = AppResources.MyVault; Content = listView; - NavigationPage.SetBackButtonTitle(this, string.Empty); + NavigationPage.SetBackButtonTitle(this, "Back"); } protected override void OnAppearing() @@ -59,25 +59,25 @@ namespace Bit.App.Pages foreach(var folder in folders) { - var f = new VaultView.Folder(folder, sites.Where(s => s.FolderId == folder.Id)); + var f = new VaultListPageModel.Folder(folder, sites.Where(s => s.FolderId == folder.Id)); Folders.Add(f); } // add the sites with no folder - var noneFolder = new VaultView.Folder(sites.Where(s => s.FolderId == null)); + var noneFolder = new VaultListPageModel.Folder(sites.Where(s => s.FolderId == null)); Folders.Add(noneFolder); } private void SiteSelected(object sender, SelectedItemChangedEventArgs e) { - var site = e.SelectedItem as VaultView.Site; + var site = e.SelectedItem as VaultListPageModel.Site; Navigation.PushAsync(new VaultViewSitePage(site.Id)); } private async void MoreClickedAsync(object sender, EventArgs e) { var mi = sender as MenuItem; - var site = mi.CommandParameter as VaultView.Site; + var site = mi.CommandParameter as VaultListPageModel.Site; var selection = await DisplayActionSheet(AppResources.MoreOptions, AppResources.Cancel, null, AppResources.View, AppResources.Edit, AppResources.CopyPassword, AppResources.CopyUsername, AppResources.GoToWebsite); @@ -117,7 +117,7 @@ namespace Bit.App.Pages } var mi = sender as MenuItem; - var site = mi.CommandParameter as VaultView.Site; + var site = mi.CommandParameter as VaultListPageModel.Site; var deleteCall = await _siteService.DeleteAsync(site.Id); if(deleteCall.Succeeded) @@ -163,8 +163,8 @@ namespace Bit.App.Pages deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding(".")); deleteAction.Clicked += page.DeleteClickedAsync; - this.SetBinding(TextProperty, s => s.Name); - this.SetBinding(DetailProperty, s => s.Username); + this.SetBinding(TextProperty, s => s.Name); + this.SetBinding(DetailProperty, s => s.Username); ContextActions.Add(moreAction); ContextActions.Add(deleteAction); } diff --git a/src/App/Pages/VaultViewSitePage.cs b/src/App/Pages/VaultViewSitePage.cs index e526e6204..f2fb4cbdd 100644 --- a/src/App/Pages/VaultViewSitePage.cs +++ b/src/App/Pages/VaultViewSitePage.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using System.Text; using Acr.UserDialogs; using Bit.App.Abstractions; +using Bit.App.Models.Page; using Bit.App.Resources; using Xamarin.Forms; using XLabs.Ioc; @@ -28,25 +25,22 @@ namespace Bit.App.Pages Init(); } - public void Init() + private VaultViewSitePageModel Model { get; set; } = new VaultViewSitePageModel(); + + private void Init() { ToolbarItems.Add(new EditSiteToolBarItem(this, _siteId)); + var stackLayout = new StackLayout(); - var site = _siteService.GetByIdAsync(_siteId).GetAwaiter().GetResult(); - if(site == null) - { - // TODO: handle error. navigate back? should never happen... - return; - } - + // Username var usernameRow = new StackLayout { Orientation = StackOrientation.Horizontal }; var usernameLabel = new Label { - Text = site.Username?.Decrypt(), HorizontalOptions = LayoutOptions.StartAndExpand, VerticalOptions = LayoutOptions.Center, LineBreakMode = LineBreakMode.TailTruncation }; + usernameLabel.SetBinding(Label.TextProperty, s => s.Username); usernameRow.Children.Add(usernameLabel); usernameRow.Children.Add(new Button { @@ -55,63 +49,66 @@ namespace Bit.App.Pages VerticalOptions = LayoutOptions.Center, Command = new Command(() => Copy(usernameLabel.Text, AppResources.Username)) }); + stackLayout.Children.Add(new Label { Text = AppResources.Username }); + stackLayout.Children.Add(usernameRow); + // Password var passwordRow = new StackLayout { Orientation = StackOrientation.Horizontal }; - var password = site.Password?.Decrypt(); var passwordLabel = new Label { - Text = new string('●', password.Length), HorizontalOptions = LayoutOptions.StartAndExpand, VerticalOptions = LayoutOptions.Center, LineBreakMode = LineBreakMode.TailTruncation }; + passwordLabel.SetBinding(Label.TextProperty, s => s.MaskedPassword); passwordRow.Children.Add(passwordLabel); var togglePasswordButton = new Button { - Text = AppResources.Show, HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Center, - Command = new Command((self) => TogglePassword(self as Button, passwordLabel, password)) + Command = new Command(() => Model.ShowPassword = !Model.ShowPassword) }; togglePasswordButton.CommandParameter = togglePasswordButton; + togglePasswordButton.SetBinding(Button.TextProperty, s => s.ShowHideText); passwordRow.Children.Add(togglePasswordButton); passwordRow.Children.Add(new Button { Text = AppResources.Copy, HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Center, - Command = new Command(() => Copy(password, AppResources.Password)) + Command = new Command(() => Copy(Model.Password, AppResources.Password)) }); + stackLayout.Children.Add(new Label { Text = AppResources.Password }); + stackLayout.Children.Add(passwordRow); + // URI var uriRow = new StackLayout { Orientation = StackOrientation.Horizontal }; - var uri = site.Uri?.Decrypt(); - uriRow.Children.Add(new Label + var uriLabel = new Label { - Text = uri, HorizontalOptions = LayoutOptions.StartAndExpand, VerticalOptions = LayoutOptions.Center, LineBreakMode = LineBreakMode.TailTruncation - }); + }; + uriLabel.SetBinding(Label.TextProperty, s => s.Uri); + uriRow.Children.Add(uriLabel); uriRow.Children.Add(new Button { Text = AppResources.Launch, HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Center, - Command = new Command(() => Device.OpenUri(new Uri(uri))) + Command = new Command(() => Device.OpenUri(new Uri(uriLabel.Text))) }); - - var stackLayout = new StackLayout(); - stackLayout.Children.Add(new Label { Text = AppResources.Username }); - stackLayout.Children.Add(usernameRow); - stackLayout.Children.Add(new Label { Text = AppResources.Password }); - stackLayout.Children.Add(passwordRow); stackLayout.Children.Add(new Label { Text = AppResources.Website }); stackLayout.Children.Add(uriRow); - if(site.Notes != null) - { - stackLayout.Children.Add(new Label { Text = AppResources.Notes }); - stackLayout.Children.Add(new Label { Text = site.Notes.Decrypt() }); - } + + // Notes + var notes = new Label { Text = AppResources.Notes }; + notes.SetBinding(Label.IsVisibleProperty, s => s.ShowNotes); + stackLayout.Children.Add(notes); + var notesLabel = new Label(); + notesLabel.SetBinding(Label.TextProperty, s => s.Notes); + notesLabel.SetBinding(Label.IsVisibleProperty, s => s.ShowNotes); + stackLayout.Children.Add(notesLabel); var scrollView = new ScrollView { @@ -119,22 +116,21 @@ namespace Bit.App.Pages Orientation = ScrollOrientation.Vertical }; - Title = site.Name?.Decrypt() ?? AppResources.SiteNoName; + SetBinding(Page.TitleProperty, new Binding("PageTitle")); Content = scrollView; + BindingContext = Model; } - private void TogglePassword(Button toggleButton, Label passwordLabel, string password) + protected override void OnAppearing() { - if(toggleButton.Text == AppResources.Show) + var site = _siteService.GetByIdAsync(_siteId).GetAwaiter().GetResult(); + if(site == null) { - toggleButton.Text = AppResources.Hide; - passwordLabel.Text = password; - } - else - { - toggleButton.Text = AppResources.Show; - passwordLabel.Text = new string('●', password.Length); + // TODO: handle error. navigate back? should never happen... + return; } + + Model.Update(site); } private void Copy(string copyText, string alertLabel)