diff --git a/src/App/App.csproj b/src/App/App.csproj index 4a770c52c..45c41f270 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -44,12 +44,17 @@ + + + + + diff --git a/src/App/Controls/DismissModalToolBarItem.cs b/src/App/Controls/DismissModalToolBarItem.cs new file mode 100644 index 000000000..9d9a9ce15 --- /dev/null +++ b/src/App/Controls/DismissModalToolBarItem.cs @@ -0,0 +1,23 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class DismissModalToolBarItem : ToolbarItem + { + private readonly ContentPage _page; + + public DismissModalToolBarItem(ContentPage page, string text = null) + { + _page = page; + Text = text ?? "Close"; + Clicked += ClickedItem; + Priority = -1; + } + + private async void ClickedItem(object sender, EventArgs e) + { + await _page.Navigation.PopModalAsync(); + } + } +} diff --git a/src/App/Controls/ExtendedNavigationPage.cs b/src/App/Controls/ExtendedNavigationPage.cs new file mode 100644 index 000000000..c67d4aeae --- /dev/null +++ b/src/App/Controls/ExtendedNavigationPage.cs @@ -0,0 +1,27 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class ExtendedNavigationPage : NavigationPage + { + public ExtendedNavigationPage() + : base() + { + SetDefaults(); + } + + public ExtendedNavigationPage(Page root) + : base(root) + { + SetDefaults(); + } + + private void SetDefaults() + { + // default colors for our app + BarBackgroundColor = Color.FromHex("3c8dbc"); + BarTextColor = Color.FromHex("ffffff"); + } + } +} diff --git a/src/App/Controls/FormEditorCell.cs b/src/App/Controls/FormEditorCell.cs new file mode 100644 index 000000000..c06e8717d --- /dev/null +++ b/src/App/Controls/FormEditorCell.cs @@ -0,0 +1,34 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class FormEditorCell : ViewCell + { + public FormEditorCell(Keyboard entryKeyboard = null, double? height = null) + { + Editor = new ExtendedEditor + { + Keyboard = entryKeyboard, + HasBorder = false + }; + + if(height.HasValue) + { + Editor.HeightRequest = height.Value; + } + + var stackLayout = new StackLayout + { + Padding = new Thickness(15, 15, 15, 0), + BackgroundColor = Color.White + }; + + stackLayout.Children.Add(Editor); + + View = stackLayout; + } + + public ExtendedEditor Editor { get; private set; } + } +} diff --git a/src/App/Controls/FormEntryCell.cs b/src/App/Controls/FormEntryCell.cs new file mode 100644 index 000000000..efd95944b --- /dev/null +++ b/src/App/Controls/FormEntryCell.cs @@ -0,0 +1,38 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class FormEntryCell : ViewCell + { + public FormEntryCell(string labelText, Keyboard entryKeyboard = null, bool IsPassword = false) + { + Label = new Label + { + Text = labelText, + FontSize = 14, + TextColor = Color.FromHex("777777") + }; + + Entry = new ExtendedEntry + { + Keyboard = entryKeyboard, + HasBorder = false + }; + + var stackLayout = new StackLayout + { + Padding = new Thickness(15, 15, 15, 0), + BackgroundColor = Color.White + }; + + stackLayout.Children.Add(Label); + stackLayout.Children.Add(Entry); + + View = stackLayout; + } + + public Label Label { get; private set; } + public ExtendedEntry Entry { get; private set; } + } +} diff --git a/src/App/Controls/FormPickerCell.cs b/src/App/Controls/FormPickerCell.cs new file mode 100644 index 000000000..953048d5f --- /dev/null +++ b/src/App/Controls/FormPickerCell.cs @@ -0,0 +1,44 @@ +using System; +using Bit.App.Resources; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class FormPickerCell : ViewCell + { + public FormPickerCell(string labelText, string[] pickerItems) + { + Label = new Label + { + Text = labelText, + FontSize = 14, + TextColor = Color.FromHex("777777") + }; + + Picker = new ExtendedPicker + { + HasBorder = false + }; + + foreach(var item in pickerItems) + { + Picker.Items.Add(item); + } + Picker.SelectedIndex = 0; + + var stackLayout = new StackLayout + { + Padding = new Thickness(15, 15, 15, 0), + BackgroundColor = Color.White + }; + + stackLayout.Children.Add(Label); + stackLayout.Children.Add(Picker); + + View = stackLayout; + } + + public Label Label { get; private set; } + public ExtendedPicker Picker { get; private set; } + } +} diff --git a/src/App/Pages/MainPage.cs b/src/App/Pages/MainPage.cs index 894f9bc19..ea1828761 100644 --- a/src/App/Pages/MainPage.cs +++ b/src/App/Pages/MainPage.cs @@ -12,12 +12,9 @@ namespace Bit.App.Pages BarTintColor = Color.FromHex("222d32"); TintColor = Color.FromHex("ffffff"); - var settingsNavigation = new NavigationPage(new SettingsPage()); - var vaultNavigation = new NavigationPage(new VaultListPage()); - var syncNavigation = new NavigationPage(new SyncPage()); - - vaultNavigation.BarBackgroundColor = settingsNavigation.BarBackgroundColor = syncNavigation.BarBackgroundColor = Color.FromHex("3c8dbc"); - vaultNavigation.BarTextColor = settingsNavigation.BarTextColor = syncNavigation.BarTextColor = Color.FromHex("ffffff"); + var settingsNavigation = new ExtendedNavigationPage(new SettingsPage()); + var vaultNavigation = new ExtendedNavigationPage(new VaultListPage()); + var syncNavigation = new ExtendedNavigationPage(new SyncPage()); vaultNavigation.Title = AppResources.MyVault; vaultNavigation.Icon = "fa-lock"; diff --git a/src/App/Pages/VaultAddSitePage.cs b/src/App/Pages/VaultAddSitePage.cs index 0464ecd0c..351d61eec 100644 --- a/src/App/Pages/VaultAddSitePage.cs +++ b/src/App/Pages/VaultAddSitePage.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Acr.UserDialogs; using Bit.App.Abstractions; @@ -30,55 +31,20 @@ namespace Bit.App.Pages private void Init() { - var folders = _folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); + var uriCell = new FormEntryCell(AppResources.URI, Keyboard.Url); + var nameCell = new FormEntryCell(AppResources.Name); + var usernameCell = new FormEntryCell(AppResources.Username); + var passwordCell = new FormEntryCell(AppResources.Password, IsPassword: true); - var uriEntry = new ExtendedEntry { Keyboard = Keyboard.Url, HasBorder = false }; - var nameEntry = new ExtendedEntry { HasBorder = false }; - var folderPicker = new ExtendedPicker { Title = AppResources.Folder, HasBorder = false }; - folderPicker.Items.Add(AppResources.FolderNone); - folderPicker.SelectedIndex = 0; + var folderOptions = new List { AppResources.FolderNone }; + var folders = _folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); foreach(var folder in folders) { - folderPicker.Items.Add(folder.Name.Decrypt()); + folderOptions.Add(folder.Name.Decrypt()); } - var usernameEntry = new ExtendedEntry { HasBorder = false }; - var passwordEntry = new ExtendedEntry { IsPassword = true, HasBorder = false }; - var notesEditor = new ExtendedEditor { HeightRequest = 90, HasBorder = false }; + var folderCell = new FormPickerCell(AppResources.Folder, folderOptions.ToArray()); - var uriStackLayout = new FormEntryStackLayout(); - uriStackLayout.Children.Add(new EntryLabel { Text = AppResources.URI }); - uriStackLayout.Children.Add(uriEntry); - var uriCell = new ViewCell(); - uriCell.View = uriStackLayout; - - var nameStackLayout = new FormEntryStackLayout(); - nameStackLayout.Children.Add(new EntryLabel { Text = AppResources.Name }); - nameStackLayout.Children.Add(nameEntry); - var nameCell = new ViewCell(); - nameCell.View = nameStackLayout; - - var folderStackLayout = new FormEntryStackLayout(); - folderStackLayout.Children.Add(new EntryLabel { Text = AppResources.Folder }); - folderStackLayout.Children.Add(folderPicker); - var folderCell = new ViewCell(); - folderCell.View = folderStackLayout; - - var usernameStackLayout = new FormEntryStackLayout(); - usernameStackLayout.Children.Add(new EntryLabel { Text = AppResources.Username }); - usernameStackLayout.Children.Add(usernameEntry); - var usernameCell = new ViewCell(); - usernameCell.View = usernameStackLayout; - - var passwordStackLayout = new FormEntryStackLayout(); - passwordStackLayout.Children.Add(new EntryLabel { Text = AppResources.Password }); - passwordStackLayout.Children.Add(passwordEntry); - var passwordCell = new ViewCell(); - passwordCell.View = passwordStackLayout; - - var notesStackLayout = new FormEntryStackLayout(); - notesStackLayout.Children.Add(notesEditor); - var notesCell = new ViewCell(); - notesCell.View = notesStackLayout; + var notesCell = new FormEditorCell(height:90); var mainTable = new ExtendedTableView { @@ -88,7 +54,7 @@ namespace Bit.App.Pages EnableSelection = false, Root = new TableRoot { - new TableSection + new TableSection("Site Information") { uriCell, nameCell, @@ -123,13 +89,13 @@ namespace Bit.App.Pages return; } - if(string.IsNullOrWhiteSpace(uriEntry.Text)) + if(string.IsNullOrWhiteSpace(uriCell.Entry.Text)) { await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.URI), AppResources.Ok); return; } - if(string.IsNullOrWhiteSpace(nameEntry.Text)) + if(string.IsNullOrWhiteSpace(nameCell.Entry.Text)) { await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.Name), AppResources.Ok); return; @@ -137,16 +103,16 @@ namespace Bit.App.Pages var site = new Site { - Uri = uriEntry.Text.Encrypt(), - Name = nameEntry.Text.Encrypt(), - Username = usernameEntry.Text?.Encrypt(), - Password = passwordEntry.Text?.Encrypt(), - Notes = notesEditor.Text?.Encrypt(), + Uri = uriCell.Entry.Text.Encrypt(), + Name = nameCell.Entry.Text.Encrypt(), + Username = usernameCell.Entry.Text?.Encrypt(), + Password = passwordCell.Entry.Text?.Encrypt(), + Notes = notesCell.Editor.Text?.Encrypt(), }; - if(folderPicker.SelectedIndex > 0) + if(folderCell.Picker.SelectedIndex > 0) { - site.FolderId = folders.ElementAt(folderPicker.SelectedIndex - 1).Id; + site.FolderId = folders.ElementAt(folderCell.Picker.SelectedIndex - 1).Id; } var saveTask = _siteService.SaveAsync(site); @@ -155,12 +121,13 @@ namespace Bit.App.Pages _userDialogs.HideLoading(); await Navigation.PopAsync(); - _userDialogs.SuccessToast(nameEntry.Text, "New site created."); + _userDialogs.SuccessToast(nameCell.Entry.Text, "New site created."); }, ToolbarItemOrder.Default, 0); Title = AppResources.AddSite; Content = scrollView; ToolbarItems.Add(saveToolBarItem); + ToolbarItems.Add(new DismissModalToolBarItem(this, "Cancel")); if(!_connectivity.IsConnected) { diff --git a/src/App/Pages/VaultEditSitePage.cs b/src/App/Pages/VaultEditSitePage.cs index 1a690f645..e1363811e 100644 --- a/src/App/Pages/VaultEditSitePage.cs +++ b/src/App/Pages/VaultEditSitePage.cs @@ -1,10 +1,9 @@ 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.Controls; using Bit.App.Resources; using Plugin.Connectivity.Abstractions; using Xamarin.Forms; @@ -40,12 +39,17 @@ namespace Bit.App.Pages return; } - var folders = _folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); + var uriCell = new FormEntryCell(AppResources.URI, Keyboard.Url); + uriCell.Entry.Text = site.Uri?.Decrypt(); + var nameCell = new FormEntryCell(AppResources.Name); + nameCell.Entry.Text = site.Name?.Decrypt(); + var usernameCell = new FormEntryCell(AppResources.Username); + usernameCell.Entry.Text = site.Username?.Decrypt(); + var passwordCell = new FormEntryCell(AppResources.Password, IsPassword: true); + passwordCell.Entry.Text = site.Password?.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); + var folderOptions = new List { AppResources.FolderNone }; + var folders = _folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt()); int selectedIndex = 0; int i = 0; foreach(var folder in folders) @@ -56,30 +60,46 @@ namespace Bit.App.Pages selectedIndex = i; } - folderPicker.Items.Add(folder.Name.Decrypt()); + folderOptions.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 folderCell = new FormPickerCell(AppResources.Folder, folderOptions.ToArray()); + folderCell.Picker.SelectedIndex = selectedIndex; - 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 notesCell = new FormEditorCell(height: 90); + notesCell.Editor.Text = site.Notes?.Decrypt(); + + var table = new ExtendedTableView + { + Intent = TableIntent.Settings, + EnableScrolling = false, + HasUnevenRows = true, + EnableSelection = false, + Root = new TableRoot + { + new TableSection("Site Information") + { + uriCell, + nameCell, + folderCell, + usernameCell, + passwordCell + }, + new TableSection(AppResources.Notes) + { + notesCell + } + } + }; + + if(Device.OS == TargetPlatform.iOS) + { + table.RowHeight = -1; + table.EstimatedRowHeight = 70; + } var scrollView = new ScrollView { - Content = stackLayout, + Content = table, Orientation = ScrollOrientation.Vertical }; @@ -91,27 +111,31 @@ namespace Bit.App.Pages return; } - if(string.IsNullOrWhiteSpace(uriEntry.Text)) + if(string.IsNullOrWhiteSpace(uriCell.Entry.Text)) { await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.URI), AppResources.Ok); return; } - if(string.IsNullOrWhiteSpace(nameEntry.Text)) + if(string.IsNullOrWhiteSpace(nameCell.Entry.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(); + site.Uri = uriCell.Entry.Text.Encrypt(); + site.Name = nameCell.Entry.Text.Encrypt(); + site.Username = usernameCell.Entry.Text?.Encrypt(); + site.Password = passwordCell.Entry.Text?.Encrypt(); + site.Notes = notesCell.Editor.Text?.Encrypt(); - if(folderPicker.SelectedIndex > 0) + if(folderCell.Picker.SelectedIndex > 0) { - site.FolderId = folders.ElementAt(folderPicker.SelectedIndex - 1).Id; + site.FolderId = folders.ElementAt(folderCell.Picker.SelectedIndex - 1).Id; + } + else + { + site.FolderId = null; } var saveTask = _siteService.SaveAsync(site); @@ -119,13 +143,14 @@ namespace Bit.App.Pages await saveTask; _userDialogs.HideLoading(); - await Navigation.PopAsync(); - _userDialogs.SuccessToast(nameEntry.Text, "Site updated."); + await Navigation.PopModalAsync(); + _userDialogs.SuccessToast(nameCell.Entry.Text, "Site updated."); }, ToolbarItemOrder.Default, 0); Title = "Edit Site"; Content = scrollView; ToolbarItems.Add(saveToolBarItem); + ToolbarItems.Add(new DismissModalToolBarItem(this, "Cancel")); if(!_connectivity.IsConnected) { diff --git a/src/App/Pages/VaultListPage.cs b/src/App/Pages/VaultListPage.cs index b944569fb..6e2fbd7fd 100644 --- a/src/App/Pages/VaultListPage.cs +++ b/src/App/Pages/VaultListPage.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Acr.UserDialogs; using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Models.Page; using Bit.App.Resources; using Xamarin.Forms; @@ -71,7 +72,8 @@ namespace Bit.App.Pages private void SiteSelected(object sender, SelectedItemChangedEventArgs e) { var site = e.SelectedItem as VaultListPageModel.Site; - Navigation.PushAsync(new VaultViewSitePage(site.Id)); + var page = new ExtendedNavigationPage(new VaultViewSitePage(site.Id)); + Navigation.PushModalAsync(page); } private async void MoreClickedAsync(object sender, EventArgs e) @@ -83,11 +85,13 @@ namespace Bit.App.Pages if(selection == AppResources.View) { - await Navigation.PushAsync(new VaultViewSitePage(site.Id)); + var page = new ExtendedNavigationPage(new VaultViewSitePage(site.Id)); + await Navigation.PushModalAsync(page); } else if(selection == AppResources.Edit) { - // TODO: navigate to edit page + var page = new ExtendedNavigationPage(new VaultEditSitePage(site.Id)); + await Navigation.PushModalAsync(page); } else if(selection == AppResources.CopyPassword) { @@ -147,7 +151,8 @@ namespace Bit.App.Pages private async void ClickedItem(object sender, EventArgs e) { - await _page.Navigation.PushAsync(new VaultAddSitePage()); + var page = new ExtendedNavigationPage(new VaultAddSitePage()); + await _page.Navigation.PushModalAsync(page); } } diff --git a/src/App/Pages/VaultViewSitePage.cs b/src/App/Pages/VaultViewSitePage.cs index 2fd33e4c0..06cec8bb1 100644 --- a/src/App/Pages/VaultViewSitePage.cs +++ b/src/App/Pages/VaultViewSitePage.cs @@ -1,6 +1,7 @@ using System; using Acr.UserDialogs; using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Models.Page; using Bit.App.Resources; using Xamarin.Forms; @@ -30,6 +31,7 @@ namespace Bit.App.Pages private void Init() { ToolbarItems.Add(new EditSiteToolBarItem(this, _siteId)); + ToolbarItems.Add(new DismissModalToolBarItem(this)); var stackLayout = new StackLayout(); // Username @@ -157,7 +159,8 @@ namespace Bit.App.Pages private async void ClickedItem(object sender, EventArgs e) { - await _page.Navigation.PushAsync(new VaultEditSitePage(_siteId)); + var page = new ExtendedNavigationPage(new VaultEditSitePage(_siteId)); + await _page.Navigation.PushModalAsync(page); } } } diff --git a/src/iOS/Controls/ContentPageRenderer.cs b/src/iOS/Controls/ContentPageRenderer.cs new file mode 100644 index 000000000..afff68785 --- /dev/null +++ b/src/iOS/Controls/ContentPageRenderer.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Bit.iOS.Controls; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportRenderer(typeof(ContentPage), typeof(ContentPageRenderer))] +namespace Bit.iOS.Controls +{ + public class ContentPageRenderer : PageRenderer + { + public override void ViewWillAppear(bool animated) + { + base.ViewWillAppear(animated); + + var contentPage = Element as ContentPage; + if(contentPage == null || NavigationController == null) + { + return; + } + + var itemsInfo = contentPage.ToolbarItems; + + var navigationItem = NavigationController.TopViewController.NavigationItem; + var leftNativeButtons = (navigationItem.LeftBarButtonItems ?? new UIBarButtonItem[] { }).ToList(); + var rightNativeButtons = (navigationItem.RightBarButtonItems ?? new UIBarButtonItem[] { }).ToList(); + + var newLeftButtons = new List(); + var newRightButtons = new List(); + + rightNativeButtons.ForEach(nativeItem => + { + // Use reflection to get Xamarin private field "_item" + var field = nativeItem.GetType().GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance); + if(field == null) + { + return; + } + + var info = field.GetValue(nativeItem) as ToolbarItem; + if(info == null) + { + return; + } + + if(info.Priority < 0) + { + newLeftButtons.Add(nativeItem); + } + else + { + newRightButtons.Add(nativeItem); + } + }); + + leftNativeButtons.ForEach(nativeItem => + { + newLeftButtons.Add(nativeItem); + }); + + navigationItem.RightBarButtonItems = newRightButtons.ToArray(); + navigationItem.LeftBarButtonItems = newLeftButtons.ToArray(); + } + } +} diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index 58f85d987..8b7db43cb 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -101,6 +101,7 @@ Entitlements.plist +