From 4ed12a859b7cde297b30e2b6fcafd3c644aa39a7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 6 May 2019 22:35:42 -0400 Subject: [PATCH] cipher searching --- src/App/Pages/BaseContentPage.cs | 6 +- src/App/Pages/Vault/CiphersPage.xaml | 19 ++--- src/App/Pages/Vault/CiphersPage.xaml.cs | 80 +++++++++++++++++-- src/App/Pages/Vault/CiphersPageViewModel.cs | 76 +++++++++++++++--- .../Vault/GroupingsPage/GroupingsPage.xaml.cs | 34 ++++---- .../GroupingsPage/GroupingsPageViewModel.cs | 11 ++- src/App/Resources/AppResources.Designer.cs | 38 ++++++++- src/App/Resources/AppResources.resx | 14 +++- src/App/Styles/Base.xaml | 37 ++++++--- src/App/Styles/Dark.xaml | 3 +- src/App/Styles/Light.xaml | 5 +- src/Core/Abstractions/ISearchService.cs | 6 +- src/Core/Services/SearchService.cs | 14 +++- 13 files changed, 270 insertions(+), 73 deletions(-) diff --git a/src/App/Pages/BaseContentPage.cs b/src/App/Pages/BaseContentPage.cs index 6fbc01825..73eb55ab2 100644 --- a/src/App/Pages/BaseContentPage.cs +++ b/src/App/Pages/BaseContentPage.cs @@ -41,17 +41,17 @@ namespace Bit.App.Pages }); } - protected void RequestFocus(Entry entry) + protected void RequestFocus(InputView input) { if(Device.RuntimePlatform == Device.iOS) { - entry.Focus(); + input.Focus(); return; } Task.Run(async () => { await Task.Delay(AndroidShowModalAnimationDelay); - Device.BeginInvokeOnMainThread(() => entry.Focus()); + Device.BeginInvokeOnMainThread(() => input.Focus()); }); } } diff --git a/src/App/Pages/Vault/CiphersPage.xaml b/src/App/Pages/Vault/CiphersPage.xaml index 99bbe5511..f88aa870d 100644 --- a/src/App/Pages/Vault/CiphersPage.xaml +++ b/src/App/Pages/Vault/CiphersPage.xaml @@ -17,7 +17,6 @@ - @@ -30,32 +29,34 @@ Spacing="0" Padding="0"> + VerticalOptions="CenterAndExpand" + Clicked="BackButton_Clicked" /> + TextChanged="SearchBar_TextChanged" + SearchButtonPressed="SearchBar_SearchButtonPressed" + Placeholder="{Binding PageTitle}" /> diff --git a/src/App/Pages/Vault/CiphersPage.xaml.cs b/src/App/Pages/Vault/CiphersPage.xaml.cs index 2c01b0448..4d01240d5 100644 --- a/src/App/Pages/Vault/CiphersPage.xaml.cs +++ b/src/App/Pages/Vault/CiphersPage.xaml.cs @@ -1,25 +1,91 @@ -using System; +using Bit.App.Resources; +using Bit.Core.Models.View; +using System; +using Xamarin.Forms; namespace Bit.App.Pages { public partial class CiphersPage : BaseContentPage { private CiphersPageViewModel _vm; + private bool _hasFocused; - public CiphersPage() + public CiphersPage(Func filter, bool folder = false, bool collection = false, + bool type = false) { InitializeComponent(); - SetActivityIndicator(); _vm = BindingContext as CiphersPageViewModel; _vm.Page = this; + _vm.Filter = filter; + if(folder) + { + _vm.PageTitle = AppResources.SearchFolder; + } + else if(collection) + { + _vm.PageTitle = AppResources.SearchCollection; + } + else if(type) + { + _vm.PageTitle = AppResources.SearchType; + } + else + { + _vm.PageTitle = AppResources.SearchVault; + } } - protected override async void OnAppearing() + public SearchBar SearchBar => _searchBar; + + protected override void OnAppearing() { base.OnAppearing(); - await LoadOnAppearedAsync(_mainLayout, true, async () => { - await _vm.LoadAsync(); - }); + if(!_hasFocused) + { + _hasFocused = true; + RequestFocus(_searchBar); + } + } + + private void SearchBar_TextChanged(object sender, TextChangedEventArgs e) + { + var oldLength = e.OldTextValue?.Length ?? 0; + var newLength = e.NewTextValue?.Length ?? 0; + if(oldLength < 2 && newLength < 2 && oldLength < newLength) + { + return; + } + _vm.Search(e.NewTextValue, 300); + } + + private void SearchBar_SearchButtonPressed(object sender, EventArgs e) + { + _vm.Search((sender as SearchBar).Text); + } + + private void BackButton_Clicked(object sender, EventArgs e) + { + GoBack(); + } + + protected override bool OnBackButtonPressed() + { + GoBack(); + return true; + } + + private void GoBack() + { + Navigation.PopModalAsync(false); + } + + private async void RowSelected(object sender, SelectedItemChangedEventArgs e) + { + ((ListView)sender).SelectedItem = null; + if(e.SelectedItem is CipherView cipher) + { + await _vm.SelectCipherAsync(cipher); + } } } } diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs index a302e5180..d167be973 100644 --- a/src/App/Pages/Vault/CiphersPageViewModel.cs +++ b/src/App/Pages/Vault/CiphersPageViewModel.cs @@ -2,6 +2,9 @@ using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; +using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Xamarin.Forms; @@ -11,23 +14,26 @@ namespace Bit.App.Pages { private readonly IPlatformUtilsService _platformUtilsService; private readonly ICipherService _cipherService; + private readonly ISearchService _searchService; + private CancellationTokenSource _searchCancellationTokenSource; private string _searchText; - private string _noDataText; private bool _showNoData; + private bool _showList; public CiphersPageViewModel() { _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _cipherService = ServiceContainer.Resolve("cipherService"); - - PageTitle = AppResources.SearchVault; + _searchService = ServiceContainer.Resolve("searchService"); + Ciphers = new ExtendedObservableCollection(); CipherOptionsCommand = new Command(CipherOptionsAsync); } public Command CipherOptionsCommand { get; set; } public ExtendedObservableCollection Ciphers { get; set; } + public Func Filter { get; set; } public string SearchText { @@ -35,23 +41,67 @@ namespace Bit.App.Pages set => SetProperty(ref _searchText, value); } - public string NoDataText - { - get => _noDataText; - set => SetProperty(ref _noDataText, value); - } - public bool ShowNoData { get => _showNoData; set => SetProperty(ref _showNoData, value); } - public async Task LoadAsync() + public bool ShowList { - var ciphers = await _cipherService.GetAllDecryptedAsync(); - Ciphers.ResetWithRange(ciphers); - ShowNoData = Ciphers.Count == 0; + get => _showList; + set => SetProperty(ref _showList, value); + } + + public void Search(string searchText, int? timeout = null) + { + var previousCts = _searchCancellationTokenSource; + var cts = new CancellationTokenSource(); + Task.Run(async () => + { + List ciphers = null; + var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1; + if(searchable) + { + if(timeout != null) + { + await Task.Delay(timeout.Value); + } + if(searchText != (Page as CiphersPage).SearchBar.Text) + { + return; + } + else + { + previousCts?.Cancel(); + } + try + { + ciphers = await _searchService.SearchCiphersAsync(searchText, Filter, null, cts.Token); + cts.Token.ThrowIfCancellationRequested(); + Ciphers.ResetWithRange(ciphers); + ShowNoData = Ciphers.Count == 0; + } + catch(OperationCanceledException) + { + ciphers = new List(); + } + } + if(ciphers == null) + { + ciphers = new List(); + } + Ciphers.ResetWithRange(ciphers); + ShowNoData = searchable && Ciphers.Count == 0; + ShowList = searchable && !ShowNoData; + }, cts.Token); + _searchCancellationTokenSource = cts; + } + + public async Task SelectCipherAsync(CipherView cipher) + { + var page = new ViewPage(cipher.Id); + await Page.Navigation.PushModalAsync(new NavigationPage(page)); } private async void CipherOptionsAsync(CipherView cipher) diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs index 8d66993d9..114cf8ddf 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs @@ -10,7 +10,7 @@ namespace Bit.App.Pages { private readonly IBroadcasterService _broadcasterService; private readonly ISyncService _syncService; - private readonly GroupingsPageViewModel _viewModel; + private readonly GroupingsPageViewModel _vm; public GroupingsPage() : this(true) @@ -23,15 +23,15 @@ namespace Bit.App.Pages SetActivityIndicator(); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _syncService = ServiceContainer.Resolve("syncService"); - _viewModel = BindingContext as GroupingsPageViewModel; - _viewModel.Page = this; - _viewModel.MainPage = mainPage; - _viewModel.Type = type; - _viewModel.FolderId = folderId; - _viewModel.CollectionId = collectionId; + _vm = BindingContext as GroupingsPageViewModel; + _vm.Page = this; + _vm.MainPage = mainPage; + _vm.Type = type; + _vm.FolderId = folderId; + _vm.CollectionId = collectionId; if(pageTitle != null) { - _viewModel.PageTitle = pageTitle; + _vm.PageTitle = pageTitle; } } @@ -52,14 +52,14 @@ namespace Bit.App.Pages { if(!_syncService.SyncInProgress) { - await _viewModel.LoadAsync(); + await _vm.LoadAsync(); } else { await Task.Delay(5000); - if(!_viewModel.Loaded) + if(!_vm.Loaded) { - await _viewModel.LoadAsync(); + await _vm.LoadAsync(); } } }); @@ -73,6 +73,7 @@ namespace Bit.App.Pages private async void RowSelected(object sender, SelectedItemChangedEventArgs e) { + ((ListView)sender).SelectedItem = null; if(!(e.SelectedItem is GroupingsPageListItem item)) { return; @@ -80,25 +81,26 @@ namespace Bit.App.Pages if(item.Cipher != null) { - await _viewModel.SelectCipherAsync(item.Cipher); + await _vm.SelectCipherAsync(item.Cipher); } else if(item.Folder != null) { - await _viewModel.SelectFolderAsync(item.Folder); + await _vm.SelectFolderAsync(item.Folder); } else if(item.Collection != null) { - await _viewModel.SelectCollectionAsync(item.Collection); + await _vm.SelectCollectionAsync(item.Collection); } else if(item.Type != null) { - await _viewModel.SelectTypeAsync(item.Type.Value); + await _vm.SelectTypeAsync(item.Type.Value); } } private async void Search_Clicked(object sender, System.EventArgs e) { - await Navigation.PushModalAsync(new NavigationPage(new CiphersPage()), false); + await Navigation.PushModalAsync(new NavigationPage( + new CiphersPage(_vm.Filter,_vm.FolderId != null, _vm.CollectionId != null, _vm.Type != null)), false); } } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs index 14f579239..1d8f2f743 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -4,6 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.View; using Bit.Core.Utilities; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -56,6 +57,7 @@ namespace Bit.App.Pages public CipherType? Type { get; set; } public string FolderId { get; set; } public string CollectionId { get; set; } + public Func Filter { get; set; } public List Ciphers { get; set; } public List FavoriteCiphers { get; set; } @@ -249,6 +251,7 @@ namespace Bit.App.Pages _folderCounts.Clear(); _collectionCounts.Clear(); _typeCounts.Clear(); + Filter = null; if(MainPage) { @@ -267,7 +270,7 @@ namespace Bit.App.Pages { if(Type != null) { - Ciphers = _allCiphers.Where(c => c.Type == Type.Value).ToList(); + Filter = c => c.Type == Type.Value; } else if(FolderId != null) { @@ -286,7 +289,7 @@ namespace Bit.App.Pages { PageTitle = AppResources.FolderNone; } - Ciphers = _allCiphers.Where(c => c.FolderId == folderId).ToList(); + Filter = c => c.FolderId == folderId; } else if(CollectionId != null) { @@ -297,13 +300,13 @@ namespace Bit.App.Pages { PageTitle = collectionNode.Node.Name; } - Ciphers = _allCiphers.Where(c => c.CollectionIds?.Contains(CollectionId) ?? false).ToList(); + Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false; } else { PageTitle = AppResources.AllItems; - Ciphers = _allCiphers; } + Ciphers = Filter != null ? _allCiphers.Where(Filter).ToList() : _allCiphers; } foreach(var c in _allCiphers) diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 3a66e5db8..a61e1bac9 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2472,6 +2472,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to There are no items to list.. + /// + public static string NoItemsToList { + get { + return ResourceManager.GetString("NoItemsToList", resourceCulture); + } + } + /// /// Looks up a localized string similar to No passwords to list.. /// @@ -2851,7 +2860,34 @@ namespace Bit.App.Resources { } /// - /// Looks up a localized string similar to Search Vault. + /// Looks up a localized string similar to Search collection. + /// + public static string SearchCollection { + get { + return ResourceManager.GetString("SearchCollection", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search folder. + /// + public static string SearchFolder { + get { + return ResourceManager.GetString("SearchFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search type. + /// + public static string SearchType { + get { + return ResourceManager.GetString("SearchType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search vault. /// public static string SearchVault { get { diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 317605182..85bb52199 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -656,7 +656,7 @@ Re-type Master Password - Search Vault + Search vault Security @@ -1396,4 +1396,16 @@ No passwords to list. + + There are no items to list. + + + Search collection + + + Search folder + + + Search type + \ No newline at end of file diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml index deec2dcf1..e4531913e 100644 --- a/src/App/Styles/Base.xaml +++ b/src/App/Styles/Base.xaml @@ -48,16 +48,6 @@ - - + + + +