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