mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-30 12:54:24 +01:00
support for showing groupings on ciphers list page
This commit is contained in:
parent
5cc1e2bb29
commit
0b1c0be0f0
@ -36,6 +36,8 @@ namespace Bit.App.Controls
|
|||||||
};
|
};
|
||||||
CountLabel.SetBinding(Label.TextProperty, string.Format("{0}.Node.{1}",
|
CountLabel.SetBinding(Label.TextProperty, string.Format("{0}.Node.{1}",
|
||||||
nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.Count)));
|
nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.Count)));
|
||||||
|
CountLabel.SetBinding(VisualElement.IsVisibleProperty, string.Format("{0}.Node.{1}",
|
||||||
|
nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.ShowCount)));
|
||||||
|
|
||||||
var stackLayout = new StackLayout
|
var stackLayout = new StackLayout
|
||||||
{
|
{
|
||||||
|
@ -217,7 +217,7 @@ namespace Bit.App.Models.Page
|
|||||||
Count = count;
|
Count = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Grouping(Folder folder, int count)
|
public Grouping(Folder folder, int? count)
|
||||||
{
|
{
|
||||||
Id = folder.Id;
|
Id = folder.Id;
|
||||||
Name = folder.Name?.Decrypt();
|
Name = folder.Name?.Decrypt();
|
||||||
@ -225,7 +225,7 @@ namespace Bit.App.Models.Page
|
|||||||
Count = count;
|
Count = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Grouping(Collection collection, int count)
|
public Grouping(Collection collection, int? count)
|
||||||
{
|
{
|
||||||
Id = collection.Id;
|
Id = collection.Id;
|
||||||
Name = collection.Name?.Decrypt(collection.OrganizationId);
|
Name = collection.Name?.Decrypt(collection.OrganizationId);
|
||||||
@ -235,9 +235,10 @@ namespace Bit.App.Models.Page
|
|||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Name { get; set; } = AppResources.FolderNone;
|
public string Name { get; set; } = AppResources.FolderNone;
|
||||||
public int Count { get; set; }
|
public int? Count { get; set; }
|
||||||
public bool Folder { get; set; }
|
public bool Folder { get; set; }
|
||||||
public bool Collection { get; set; }
|
public bool Collection { get; set; }
|
||||||
|
public bool ShowCount => Count.HasValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ namespace Bit.App.Pages
|
|||||||
private readonly IAppSettingsService _appSettingsService;
|
private readonly IAppSettingsService _appSettingsService;
|
||||||
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IFolderService _folderService;
|
||||||
|
private readonly ICollectionService _collectionService;
|
||||||
private CancellationTokenSource _filterResultsCancellationTokenSource;
|
private CancellationTokenSource _filterResultsCancellationTokenSource;
|
||||||
private readonly bool _favorites = false;
|
private readonly bool _favorites = false;
|
||||||
private readonly bool _folder = false;
|
private readonly bool _folder = false;
|
||||||
@ -52,13 +54,16 @@ namespace Bit.App.Pages
|
|||||||
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
|
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
|
||||||
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
|
||||||
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
|
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
|
||||||
|
_folderService = Resolver.Resolve<IFolderService>();
|
||||||
|
_collectionService = Resolver.Resolve<ICollectionService>();
|
||||||
|
|
||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtendedObservableCollection<Section<Cipher>> PresentationSections { get; private set; }
|
public ExtendedObservableCollection<Section<GroupingOrCipher>> PresentationSections { get; private set; }
|
||||||
= new ExtendedObservableCollection<Section<Cipher>>();
|
= new ExtendedObservableCollection<Section<GroupingOrCipher>>();
|
||||||
public Cipher[] Ciphers { get; set; } = new Cipher[] { };
|
public Cipher[] Ciphers { get; set; } = new Cipher[] { };
|
||||||
|
public GroupingOrCipher[] Groupings { get; set; } = new GroupingOrCipher[] { };
|
||||||
public ExtendedListView ListView { get; set; }
|
public ExtendedListView ListView { get; set; }
|
||||||
public SearchBar Search { get; set; }
|
public SearchBar Search { get; set; }
|
||||||
public ActivityIndicator LoadingIndicator { get; set; }
|
public ActivityIndicator LoadingIndicator { get; set; }
|
||||||
@ -75,11 +80,10 @@ namespace Bit.App.Pages
|
|||||||
IsGroupingEnabled = true,
|
IsGroupingEnabled = true,
|
||||||
ItemsSource = PresentationSections,
|
ItemsSource = PresentationSections,
|
||||||
HasUnevenRows = true,
|
HasUnevenRows = true,
|
||||||
GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(nameof(Section<Cipher>.Name),
|
GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(
|
||||||
nameof(Section<Cipher>.Count))),
|
nameof(Section<GroupingOrCipher>.Name), nameof(Section<GroupingOrCipher>.Count))),
|
||||||
GroupShortNameBinding = new Binding(nameof(Section<Cipher>.Name)),
|
GroupShortNameBinding = new Binding(nameof(Section<GroupingOrCipher>.Name)),
|
||||||
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
|
ItemTemplate = new GroupingOrCipherDataTemplateSelector(this)
|
||||||
(Cipher c) => Helpers.CipherMoreClickedAsync(this, c, !string.IsNullOrWhiteSpace(_uri))))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if(Device.RuntimePlatform == Device.iOS)
|
if(Device.RuntimePlatform == Device.iOS)
|
||||||
@ -250,7 +254,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(searchFilter))
|
if(string.IsNullOrWhiteSpace(searchFilter))
|
||||||
{
|
{
|
||||||
LoadSections(Ciphers, ct);
|
LoadSections(Ciphers, Groupings, ct);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -263,7 +267,7 @@ namespace Bit.App.Pages
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
LoadSections(filteredCiphers, ct);
|
LoadSections(filteredCiphers, null, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +295,7 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddCipherItem?.InitEvents();
|
AddCipherItem?.InitEvents();
|
||||||
ListView.ItemSelected += CipherSelected;
|
ListView.ItemSelected += GroupingOrCipherSelected;
|
||||||
Search.TextChanged += SearchBar_TextChanged;
|
Search.TextChanged += SearchBar_TextChanged;
|
||||||
Search.SearchButtonPressed += SearchBar_SearchButtonPressed;
|
Search.SearchButtonPressed += SearchBar_SearchButtonPressed;
|
||||||
_filterResultsCancellationTokenSource = FetchAndLoadVault();
|
_filterResultsCancellationTokenSource = FetchAndLoadVault();
|
||||||
@ -303,7 +307,7 @@ namespace Bit.App.Pages
|
|||||||
MessagingCenter.Unsubscribe<Application, bool>(Application.Current, "SyncCompleted");
|
MessagingCenter.Unsubscribe<Application, bool>(Application.Current, "SyncCompleted");
|
||||||
|
|
||||||
AddCipherItem?.Dispose();
|
AddCipherItem?.Dispose();
|
||||||
ListView.ItemSelected -= CipherSelected;
|
ListView.ItemSelected -= GroupingOrCipherSelected;
|
||||||
Search.TextChanged -= SearchBar_TextChanged;
|
Search.TextChanged -= SearchBar_TextChanged;
|
||||||
Search.SearchButtonPressed -= SearchBar_SearchButtonPressed;
|
Search.SearchButtonPressed -= SearchBar_SearchButtonPressed;
|
||||||
}
|
}
|
||||||
@ -324,10 +328,28 @@ namespace Bit.App.Pages
|
|||||||
if(_folder || !string.IsNullOrWhiteSpace(_folderId))
|
if(_folder || !string.IsNullOrWhiteSpace(_folderId))
|
||||||
{
|
{
|
||||||
ciphers = await _cipherService.GetAllByFolderAsync(_folderId);
|
ciphers = await _cipherService.GetAllByFolderAsync(_folderId);
|
||||||
|
|
||||||
|
var folders = await _folderService.GetAllAsync();
|
||||||
|
var fGroupings = folders.Select(f => new Grouping(f, null)).OrderBy(g => g.Name).ToList();
|
||||||
|
var fTreeNodes = Helpers.GetAllNested(fGroupings);
|
||||||
|
var fTreeNode = Helpers.GetTreeNodeObject(fTreeNodes, _folderId);
|
||||||
|
if(fTreeNode.Children?.Any() ?? false)
|
||||||
|
{
|
||||||
|
Groupings = fTreeNode.Children.Select(n => new GroupingOrCipher(n)).ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(!string.IsNullOrWhiteSpace(_collectionId))
|
else if(!string.IsNullOrWhiteSpace(_collectionId))
|
||||||
{
|
{
|
||||||
ciphers = await _cipherService.GetAllByCollectionAsync(_collectionId);
|
ciphers = await _cipherService.GetAllByCollectionAsync(_collectionId);
|
||||||
|
|
||||||
|
var collections = await _collectionService.GetAllAsync();
|
||||||
|
var cGroupings = collections.Select(c => new Grouping(c, null)).OrderBy(g => g.Name).ToList();
|
||||||
|
var cTreeNodes = Helpers.GetAllNested(cGroupings);
|
||||||
|
var cTreeNode = Helpers.GetTreeNodeObject(cTreeNodes, _collectionId);
|
||||||
|
if(cTreeNode.Children?.Any() ?? false)
|
||||||
|
{
|
||||||
|
Groupings = cTreeNode.Children.Select(n => new GroupingOrCipher(n)).ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(_favorites)
|
else if(_favorites)
|
||||||
{
|
{
|
||||||
@ -363,11 +385,20 @@ namespace Bit.App.Pages
|
|||||||
return cts;
|
return cts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadSections(Cipher[] ciphers, CancellationToken ct)
|
private void LoadSections(Cipher[] ciphers, GroupingOrCipher[] groupings, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var sections = ciphers.GroupBy(c => c.NameGroup.ToUpperInvariant())
|
var sections = ciphers.GroupBy(c => c.NameGroup.ToUpperInvariant())
|
||||||
.Select(g => new Section<Cipher>(g.ToList(), g.Key));
|
.Select(g => new Section<GroupingOrCipher>(g.Select(g2 => new GroupingOrCipher(g2)).ToList(), g.Key))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if(groupings?.Any() ?? false)
|
||||||
|
{
|
||||||
|
sections.Insert(0, new Section<GroupingOrCipher>(groupings.ToList(),
|
||||||
|
_folder ? AppResources.Folders : AppResources.Collections));
|
||||||
|
}
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
@ -393,79 +424,122 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void CipherSelected(object sender, SelectedItemChangedEventArgs e)
|
private async void GroupingOrCipherSelected(object sender, SelectedItemChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var cipher = e.SelectedItem as Cipher;
|
var groupingOrCipher = e.SelectedItem as GroupingOrCipher;
|
||||||
if(cipher == null)
|
if(groupingOrCipher == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string selection = null;
|
if(groupingOrCipher.Grouping != null)
|
||||||
if(!string.IsNullOrWhiteSpace(_uri))
|
|
||||||
{
|
{
|
||||||
var options = new List<string> { AppResources.Autofill };
|
Page page;
|
||||||
if(cipher.Type == Enums.CipherType.Login && _connectivity.IsConnected)
|
if(groupingOrCipher.Grouping.Node.Folder)
|
||||||
{
|
{
|
||||||
options.Add(AppResources.AutofillAndSave);
|
page = new VaultListCiphersPage(folder: true,
|
||||||
}
|
folderId: groupingOrCipher.Grouping.Node.Id, groupingName: groupingOrCipher.Grouping.Node.Name);
|
||||||
options.Add(AppResources.View);
|
|
||||||
selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
|
|
||||||
options.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri))
|
|
||||||
{
|
|
||||||
var page = new VaultViewCipherPage(cipher.Type, cipher.Id);
|
|
||||||
await Navigation.PushForDeviceAsync(page);
|
|
||||||
}
|
|
||||||
else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
|
||||||
{
|
|
||||||
if(selection == AppResources.AutofillAndSave)
|
|
||||||
{
|
|
||||||
if(!_connectivity.IsConnected)
|
|
||||||
{
|
|
||||||
Helpers.AlertNoConnection(this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var uris = cipher.CipherModel.Login?.Uris?.ToList();
|
|
||||||
if(uris == null)
|
|
||||||
{
|
|
||||||
uris = new List<Models.LoginUri>();
|
|
||||||
}
|
|
||||||
|
|
||||||
uris.Add(new Models.LoginUri
|
|
||||||
{
|
|
||||||
Uri = _uri.Encrypt(cipher.CipherModel.OrganizationId),
|
|
||||||
Match = null
|
|
||||||
});
|
|
||||||
|
|
||||||
cipher.CipherModel.Login.Uris = uris;
|
|
||||||
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
|
||||||
var saveTask = await _cipherService.SaveAsync(cipher.CipherModel);
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
if(saveTask.Succeeded)
|
|
||||||
{
|
|
||||||
_googleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_deviceInfoService.Version < 21)
|
|
||||||
{
|
|
||||||
Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_googleAnalyticsService.TrackExtensionEvent("AutoFilled",
|
page = new VaultListCiphersPage(collectionId: groupingOrCipher.Grouping.Node.Id,
|
||||||
_uri.StartsWith("http") ? "Website" : "App");
|
groupingName: groupingOrCipher.Grouping.Node.Name);
|
||||||
_deviceActionService.Autofill(cipher);
|
}
|
||||||
|
|
||||||
|
await Navigation.PushAsync(page);
|
||||||
|
}
|
||||||
|
else if(groupingOrCipher.Cipher != null)
|
||||||
|
{
|
||||||
|
var cipher = groupingOrCipher.Cipher;
|
||||||
|
string selection = null;
|
||||||
|
if(!string.IsNullOrWhiteSpace(_uri))
|
||||||
|
{
|
||||||
|
var options = new List<string> { AppResources.Autofill };
|
||||||
|
if(cipher.Type == Enums.CipherType.Login && _connectivity.IsConnected)
|
||||||
|
{
|
||||||
|
options.Add(AppResources.AutofillAndSave);
|
||||||
|
}
|
||||||
|
options.Add(AppResources.View);
|
||||||
|
selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
|
||||||
|
options.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri))
|
||||||
|
{
|
||||||
|
var page = new VaultViewCipherPage(cipher.Type, cipher.Id);
|
||||||
|
await Navigation.PushForDeviceAsync(page);
|
||||||
|
}
|
||||||
|
else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
||||||
|
{
|
||||||
|
if(selection == AppResources.AutofillAndSave)
|
||||||
|
{
|
||||||
|
if(!_connectivity.IsConnected)
|
||||||
|
{
|
||||||
|
Helpers.AlertNoConnection(this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var uris = cipher.CipherModel.Login?.Uris?.ToList();
|
||||||
|
if(uris == null)
|
||||||
|
{
|
||||||
|
uris = new List<Models.LoginUri>();
|
||||||
|
}
|
||||||
|
|
||||||
|
uris.Add(new Models.LoginUri
|
||||||
|
{
|
||||||
|
Uri = _uri.Encrypt(cipher.CipherModel.OrganizationId),
|
||||||
|
Match = null
|
||||||
|
});
|
||||||
|
|
||||||
|
cipher.CipherModel.Login.Uris = uris;
|
||||||
|
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||||
|
var saveTask = await _cipherService.SaveAsync(cipher.CipherModel);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
if(saveTask.Succeeded)
|
||||||
|
{
|
||||||
|
_googleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_deviceInfoService.Version < 21)
|
||||||
|
{
|
||||||
|
Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_googleAnalyticsService.TrackExtensionEvent("AutoFilled",
|
||||||
|
_uri.StartsWith("http") ? "Website" : "App");
|
||||||
|
_deviceActionService.Autofill(cipher);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
((ListView)sender).SelectedItem = null;
|
((ListView)sender).SelectedItem = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class GroupingOrCipherDataTemplateSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public GroupingOrCipherDataTemplateSelector(VaultListCiphersPage page)
|
||||||
|
{
|
||||||
|
GroupingTemplate = new DataTemplate(() => new VaultGroupingViewCell());
|
||||||
|
CipherTemplate = new DataTemplate(() => new VaultListViewCell(
|
||||||
|
(Cipher c) => Helpers.CipherMoreClickedAsync(page, c, !string.IsNullOrWhiteSpace(page._uri)),
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataTemplate GroupingTemplate { get; set; }
|
||||||
|
public DataTemplate CipherTemplate { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||||
|
{
|
||||||
|
if(item == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ((GroupingOrCipher)item).Cipher == null ? GroupingTemplate : CipherTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user