cipher searching

This commit is contained in:
Kyle Spearrin 2019-05-06 22:35:42 -04:00
parent 128935eb9f
commit 4ed12a859b
13 changed files with 270 additions and 73 deletions

View File

@ -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());
});
}
}

View File

@ -17,7 +17,6 @@
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:DateTimeConverter x:Key="dateTime" />
</ResourceDictionary>
</ContentPage.Resources>
@ -30,32 +29,34 @@
Spacing="0"
Padding="0">
<controls:MiButton
StyleClass="btn-title"
Text="&#xe5c4;"
BackgroundColor="Transparent"
Padding="0"
WidthRequest="37"
FontSize="25"
VerticalOptions="CenterAndExpand" />
VerticalOptions="CenterAndExpand"
Clicked="BackButton_Clicked" />
<SearchBar
x:Name="_searchBar"
HorizontalOptions="FillAndExpand"
BackgroundColor="Transparent"
Placeholder="{u:I18n SearchVault}" />
TextChanged="SearchBar_TextChanged"
SearchButtonPressed="SearchBar_SearchButtonPressed"
Placeholder="{Binding PageTitle}" />
</StackLayout>
</NavigationPage.TitleView>
<StackLayout x:Name="_mainLayout">
<Label IsVisible="{Binding ShowNoData}"
Text="{Binding NoDataText}"
Text="{u:I18n NoItemsToList}"
Margin="20, 0"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"></Label>
<ListView x:Name="_listView"
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
IsVisible="{Binding ShowList}"
ItemsSource="{Binding Ciphers}"
VerticalOptions="FillAndExpand"
HasUnevenRows="true"
CachingStrategy="RecycleElement"
ItemSelected="RowSelected"
StyleClass="list, list-platform">
<ListView.ItemTemplate>
<DataTemplate x:DataType="views:CipherView">

View File

@ -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<CipherView, bool> 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);
}
}
}
}

View File

@ -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<IPlatformUtilsService>("platformUtilsService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
PageTitle = AppResources.SearchVault;
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
Ciphers = new ExtendedObservableCollection<CipherView>();
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
}
public Command CipherOptionsCommand { get; set; }
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
public Func<CipherView, bool> 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<CipherView> 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<CipherView>();
}
}
if(ciphers == null)
{
ciphers = new List<CipherView>();
}
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)

View File

@ -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<IBroadcasterService>("broadcasterService");
_syncService = ServiceContainer.Resolve<ISyncService>("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);
}
}
}

View File

@ -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<CipherView, bool> Filter { get; set; }
public List<CipherView> Ciphers { get; set; }
public List<CipherView> 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)

View File

@ -2472,6 +2472,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to There are no items to list..
/// </summary>
public static string NoItemsToList {
get {
return ResourceManager.GetString("NoItemsToList", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No passwords to list..
/// </summary>
@ -2851,7 +2860,34 @@ namespace Bit.App.Resources {
}
/// <summary>
/// Looks up a localized string similar to Search Vault.
/// Looks up a localized string similar to Search collection.
/// </summary>
public static string SearchCollection {
get {
return ResourceManager.GetString("SearchCollection", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search folder.
/// </summary>
public static string SearchFolder {
get {
return ResourceManager.GetString("SearchFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search type.
/// </summary>
public static string SearchType {
get {
return ResourceManager.GetString("SearchType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search vault.
/// </summary>
public static string SearchVault {
get {

View File

@ -656,7 +656,7 @@
<value>Re-type Master Password</value>
</data>
<data name="SearchVault" xml:space="preserve">
<value>Search Vault</value>
<value>Search vault</value>
</data>
<data name="Security" xml:space="preserve">
<value>Security</value>
@ -1396,4 +1396,16 @@
<data name="NoPasswordsToList" xml:space="preserve">
<value>No passwords to list.</value>
</data>
<data name="NoItemsToList" xml:space="preserve">
<value>There are no items to list.</value>
</data>
<data name="SearchCollection" xml:space="preserve">
<value>Search collection</value>
</data>
<data name="SearchFolder" xml:space="preserve">
<value>Search folder</value>
</data>
<data name="SearchType" xml:space="preserve">
<value>Search type</value>
</data>
</root>

View File

@ -48,16 +48,6 @@
<Setter Property="FontAttributes"
Value="Bold" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="TextColor"
Value="{StaticResource HeaderEntryTextColor}" />
<Setter Property="CancelButtonColor"
Value="{StaticResource HeaderEntryTextColor}" />
<Setter Property="PlaceholderColor"
Value="{StaticResource HeaderEntryPlaceholderColor}" />
</Style>
<!-- Buttons -->
<Style TargetType="Button"
@ -79,7 +69,32 @@
Value="{StaticResource DisabledIconColor}" />
</Style>
<!-- Title -->
<Style TargetType="Button"
Class="btn-title"
ApplyToDerivedTypes="True">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="Padding"
Value="0" />
<Setter Property="WidthRequest"
Value="37" />
<Setter Property="FontSize"
Value="25" />
<Setter Property="TextColor"
Value="{StaticResource TitleTextColor}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="TextColor"
Value="{StaticResource TitleEntryTextColor}" />
<Setter Property="CancelButtonColor"
Value="{StaticResource TitleEntryTextColor}" />
<Setter Property="PlaceholderColor"
Value="{StaticResource TitleEntryPlaceholderColor}" />
</Style>
<!-- List -->
<Style TargetType="ListView"
Class="list">

View File

@ -17,7 +17,8 @@
<Color x:Key="BoxBorderColor">#f0f0f0</Color>
<Color x:Key="BoxHeaderTextColor">#3c8dbc</Color>
<Color x:Key="HeaderTextColor">#ffffff</Color>
<Color x:Key="HeaderEntryTextColor">#ffffff</Color>
<Color x:Key="HeaderEntryPlaceholderColor">#707070</Color>

View File

@ -18,8 +18,9 @@
<Color x:Key="BoxBorderColor">#dddddd</Color>
<Color x:Key="BoxHeaderTextColor">#3c8dbc</Color>
<Color x:Key="HeaderEntryTextColor">#ffffff</Color>
<Color x:Key="HeaderEntryPlaceholderColor">#c0dbeb</Color>
<Color x:Key="TitleTextColor">#ffffff</Color>
<Color x:Key="TitleEntryTextColor">#ffffff</Color>
<Color x:Key="TitleEntryPlaceholderColor">#c0dbeb</Color>
<Color x:Key="ListItemBorderColor">#f0f0f0</Color>
<Color x:Key="ListHeaderTextColor">#3c8dbc</Color>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Models.View;
@ -11,7 +12,8 @@ namespace Bit.Core.Abstractions
Task IndexCiphersAsync();
bool IsSearchable(string query);
Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
List<CipherView> ciphers = null);
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query);
List<CipherView> ciphers = null, CancellationToken ct = default(CancellationToken));
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
CancellationToken ct = default(CancellationToken));
}
}

View File

@ -3,6 +3,7 @@ using Bit.Core.Models.View;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Bit.Core.Services
@ -24,7 +25,7 @@ namespace Bit.Core.Services
public bool IsSearchable(string query)
{
return true;
return (query?.Length ?? 0) > 1;
}
public Task IndexCiphersAsync()
@ -34,7 +35,7 @@ namespace Bit.Core.Services
}
public async Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
List<CipherView> ciphers = null)
List<CipherView> ciphers = null, CancellationToken ct = default(CancellationToken))
{
var results = new List<CipherView>();
if(query != null)
@ -49,10 +50,14 @@ namespace Bit.Core.Services
{
ciphers = await _cipherService.GetAllDecryptedAsync();
}
ct.ThrowIfCancellationRequested();
if(filter != null)
{
ciphers = ciphers.Where(filter).ToList();
}
ct.ThrowIfCancellationRequested();
if(!IsSearchable(query))
{
return ciphers;
@ -62,11 +67,14 @@ namespace Bit.Core.Services
// TODO: advanced searching with index
}
public List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query)
public List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
CancellationToken ct = default(CancellationToken))
{
ct.ThrowIfCancellationRequested();
query = query.Trim().ToLower();
return ciphers.Where(c =>
{
ct.ThrowIfCancellationRequested();
if(c.Name?.ToLower().Contains(query) ?? false)
{
return true;