diff --git a/src/App/Controls/VaultGroupingViewCell.cs b/src/App/Controls/VaultGroupingViewCell.cs index e41613d3c..82b4aad1c 100644 --- a/src/App/Controls/VaultGroupingViewCell.cs +++ b/src/App/Controls/VaultGroupingViewCell.cs @@ -28,7 +28,8 @@ namespace Bit.App.Controls FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), HorizontalOptions = LayoutOptions.StartAndExpand }; - Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Grouping.Name)); + Label.SetBinding(Label.TextProperty, string.Format("{0}.{1}", + nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.Name))); CountLabel = new Label { @@ -37,7 +38,8 @@ namespace Bit.App.Controls Style = (Style)Application.Current.Resources["text-muted"], HorizontalOptions = LayoutOptions.End }; - CountLabel.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Grouping.Count)); + CountLabel.SetBinding(Label.TextProperty, string.Format("{0}.{1}", + nameof(VaultListPageModel.GroupingOrCipher.Grouping), nameof(VaultListPageModel.Grouping.Count))); var stackLayout = new StackLayout { @@ -68,9 +70,10 @@ namespace Bit.App.Controls protected override void OnBindingContextChanged() { - if(BindingContext is VaultListPageModel.Grouping grouping) + if(BindingContext is VaultListPageModel.GroupingOrCipher model) { - Icon.Source = grouping.Folder ? $"folder{(grouping.Id == null ? "_o" : string.Empty)}.png" : "cube.png"; + Icon.Source = model.Grouping.Folder ? + $"folder{(model.Grouping.Id == null ? "_o" : string.Empty)}.png" : "cube.png"; } base.OnBindingContextChanged(); diff --git a/src/App/Controls/VaultListViewCell.cs b/src/App/Controls/VaultListViewCell.cs index 44ae58ec3..f0bedf041 100644 --- a/src/App/Controls/VaultListViewCell.cs +++ b/src/App/Controls/VaultListViewCell.cs @@ -8,17 +8,33 @@ namespace Bit.App.Controls { public static readonly BindableProperty CipherParameterProperty = BindableProperty.Create(nameof(CipherParameter), typeof(VaultListPageModel.Cipher), typeof(VaultListViewCell), null); + public static readonly BindableProperty GroupingOrCipherParameterProperty = + BindableProperty.Create(nameof(GroupingOrCipherParameter), typeof(VaultListPageModel.GroupingOrCipher), + typeof(VaultListViewCell), null); - public VaultListViewCell(Action moreClickedAction) + public VaultListViewCell(Action moreClickedAction, bool groupingOrCipherBinding = false) { - SetBinding(CipherParameterProperty, new Binding(".")); - Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Cipher.Name)); - Detail.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Cipher.Subtitle)); - LabelIcon.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Cipher.Shared)); - LabelIcon2.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Cipher.HasAttachments)); + string bindingPrefix = null; + if(groupingOrCipherBinding) + { + SetBinding(GroupingOrCipherParameterProperty, new Binding(".")); + bindingPrefix = string.Concat(nameof(VaultListPageModel.GroupingOrCipher.Cipher), "."); + Button.Command = new Command(() => moreClickedAction?.Invoke(GroupingOrCipherParameter?.Cipher)); + } + else + { + SetBinding(CipherParameterProperty, new Binding(".")); + Button.Command = new Command(() => moreClickedAction?.Invoke(CipherParameter)); + } + + Label.SetBinding(Label.TextProperty, bindingPrefix + nameof(VaultListPageModel.Cipher.Name)); + Detail.SetBinding(Label.TextProperty, bindingPrefix + nameof(VaultListPageModel.Cipher.Subtitle)); + LabelIcon.SetBinding(VisualElement.IsVisibleProperty, + bindingPrefix + nameof(VaultListPageModel.Cipher.Shared)); + LabelIcon2.SetBinding(VisualElement.IsVisibleProperty, + bindingPrefix + nameof(VaultListPageModel.Cipher.HasAttachments)); Button.Image = "more.png"; - Button.Command = new Command(() => moreClickedAction?.Invoke(CipherParameter)); Button.BackgroundColor = Color.Transparent; LabelIcon.Source = "share.png"; @@ -33,16 +49,33 @@ namespace Bit.App.Controls set { SetValue(CipherParameterProperty, value); } } + public VaultListPageModel.GroupingOrCipher GroupingOrCipherParameter + { + get { return GetValue(GroupingOrCipherParameterProperty) as VaultListPageModel.GroupingOrCipher; } + set { SetValue(GroupingOrCipherParameterProperty, value); } + } + protected override void OnBindingContextChanged() { Icon.Source = null; + + VaultListPageModel.Cipher cipher = null; if(BindingContext is VaultListPageModel.Cipher item) { - if(item.Type == Enums.CipherType.Login) + cipher = item; + } + else if(BindingContext is VaultListPageModel.GroupingOrCipher groupingOrCipherItem) + { + cipher = groupingOrCipherItem.Cipher; + } + + if(cipher != null) + { + if(cipher.Type == Enums.CipherType.Login) { Icon.LoadingPlaceholder = "login.png"; } - Icon.Source = item.Icon; + Icon.Source = cipher.Icon; } base.OnBindingContextChanged(); diff --git a/src/App/Models/Page/VaultListPageModel.cs b/src/App/Models/Page/VaultListPageModel.cs index 234fa9a09..1c84d3b84 100644 --- a/src/App/Models/Page/VaultListPageModel.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -185,6 +185,24 @@ namespace Bit.App.Models.Page public string Name { get; set; } } + public class GroupingOrCipher + { + public GroupingOrCipher(Grouping grouping) + { + Grouping = grouping; + Cipher = null; + } + + public GroupingOrCipher(Cipher cipher) + { + Cipher = cipher; + Grouping = null; + } + + public Grouping Grouping { get; set; } + public Cipher Cipher { get; set; } + } + public class Grouping { public Grouping(string name, int count) diff --git a/src/App/Pages/MainPage.cs b/src/App/Pages/MainPage.cs index 89b99e5f9..dcd2a2e36 100644 --- a/src/App/Pages/MainPage.cs +++ b/src/App/Pages/MainPage.cs @@ -13,16 +13,13 @@ namespace Bit.App.Pages TintColor = Color.FromHex("3c8dbc"); var settingsNavigation = new ExtendedNavigationPage(new SettingsPage()); - var favoritesNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(favorites: true)); var vaultNavigation = new ExtendedNavigationPage(new VaultListGroupingsPage()); var toolsNavigation = new ExtendedNavigationPage(new ToolsPage()); - favoritesNavigation.Icon = "star.png"; vaultNavigation.Icon = "fa_lock.png"; toolsNavigation.Icon = "tools.png"; settingsNavigation.Icon = "cogs.png"; - Children.Add(favoritesNavigation); Children.Add(vaultNavigation); Children.Add(toolsNavigation); Children.Add(settingsNavigation); diff --git a/src/App/Pages/Vault/VaultListGroupingsPage.cs b/src/App/Pages/Vault/VaultListGroupingsPage.cs index 35b083d58..f5987086a 100644 --- a/src/App/Pages/Vault/VaultListGroupingsPage.cs +++ b/src/App/Pages/Vault/VaultListGroupingsPage.cs @@ -51,8 +51,8 @@ namespace Bit.App.Pages Init(); } - public ExtendedObservableCollection> PresentationSections { get; private set; } - = new ExtendedObservableCollection>(); + public ExtendedObservableCollection> PresentationSections { get; private set; } + = new ExtendedObservableCollection>(); public ListView ListView { get; set; } public StackLayout NoDataStackLayout { get; set; } public ActivityIndicator LoadingIndicator { get; set; } @@ -73,7 +73,7 @@ namespace Bit.App.Pages HasUnevenRows = true, GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( nameof(Section.Name), nameof(Section.Count), new Thickness(16, 12))), - ItemTemplate = new DataTemplate(() => new VaultGroupingViewCell()) + ItemTemplate = new GroupingOrCipherDataTemplateSelector(this) }; if(Device.RuntimePlatform == Device.iOS) @@ -130,7 +130,7 @@ namespace Bit.App.Pages } }); - ListView.ItemSelected += GroupingSelected; + ListView.ItemSelected += GroupingOrCipherSelected; AddCipherItem?.InitEvents(); SearchItem?.InitEvents(); @@ -173,7 +173,7 @@ namespace Bit.App.Pages base.OnDisappearing(); MessagingCenter.Unsubscribe(Application.Current, "SyncCompleted"); - ListView.ItemSelected -= GroupingSelected; + ListView.ItemSelected -= GroupingOrCipherSelected; AddCipherItem?.Dispose(); SearchItem?.Dispose(); } @@ -185,7 +185,8 @@ namespace Bit.App.Pages Task.Run(async () => { - var sections = new List>(); + var sections = new List>(); + var favoriteCipherGroupings = new List(); var ciphers = await _cipherService.GetAllAsync(); var collectionsDict = (await _collectionService.GetAllCipherAssociationsAsync()) .GroupBy(c => c.Item2).ToDictionary(g => g.Key, v => v.ToList()); @@ -193,6 +194,11 @@ namespace Bit.App.Pages var folderCounts = new Dictionary { ["none"] = 0 }; foreach(var cipher in ciphers) { + if(cipher.Favorite) + { + favoriteCipherGroupings.Add(new GroupingOrCipher(new Cipher(cipher, _appSettingsService))); + } + if(cipher.FolderId != null) { if(!folderCounts.ContainsKey(cipher.FolderId)) @@ -207,21 +213,28 @@ namespace Bit.App.Pages } } + if(favoriteCipherGroupings?.Any() ?? false) + { + sections.Add(new Section( + favoriteCipherGroupings.OrderBy(g => g.Cipher.Name).ThenBy(g => g.Cipher.Subtitle).ToList(), + AppResources.Favorites)); + } + var folders = await _folderService.GetAllAsync(); var folderGroupings = folders? - .Select(f => new Grouping(f, folderCounts.ContainsKey(f.Id) ? folderCounts[f.Id] : 0)) - .OrderBy(g => g.Name).ToList(); - folderGroupings.Add(new Grouping(AppResources.FolderNone, folderCounts["none"])); - sections.Add(new Section(folderGroupings, AppResources.Folders)); + .Select(f => new GroupingOrCipher(new Grouping(f, folderCounts.ContainsKey(f.Id) ? folderCounts[f.Id] : 0))) + .OrderBy(g => g.Grouping.Name).ToList(); + folderGroupings.Add(new GroupingOrCipher(new Grouping(AppResources.FolderNone, folderCounts["none"]))); + sections.Add(new Section(folderGroupings, AppResources.Folders)); var collections = await _collectionService.GetAllAsync(); var collectionGroupings = collections? - .Select(c => new Grouping(c, - collectionsDict.ContainsKey(c.Id) ? collectionsDict[c.Id].Count() : 0)) - .OrderBy(g => g.Name).ToList(); + .Select(c => new GroupingOrCipher(new Grouping(c, + collectionsDict.ContainsKey(c.Id) ? collectionsDict[c.Id].Count() : 0))) + .OrderBy(g => g.Grouping.Name).ToList(); if(collectionGroupings?.Any() ?? false) { - sections.Add(new Section(collectionGroupings, AppResources.Collections)); + sections.Add(new Section(collectionGroupings, AppResources.Collections)); } Device.BeginInvokeOnMainThread(() => @@ -246,25 +259,36 @@ namespace Bit.App.Pages return cts; } - private async void GroupingSelected(object sender, SelectedItemChangedEventArgs e) + private async void GroupingOrCipherSelected(object sender, SelectedItemChangedEventArgs e) { - var grouping = e.SelectedItem as Grouping; - if(grouping == null) + var groupingOrCipher = e.SelectedItem as GroupingOrCipher; + if(groupingOrCipher == null) { return; } - Page page; - if(grouping.Folder) + if(groupingOrCipher.Grouping != null) { - page = new VaultListCiphersPage(folder: true, folderId: grouping.Id, groupingName: grouping.Name); + Page page; + if(groupingOrCipher.Grouping.Folder) + { + page = new VaultListCiphersPage(folder: true, + folderId: groupingOrCipher.Grouping.Id, groupingName: groupingOrCipher.Grouping.Name); + } + else + { + page = new VaultListCiphersPage(collectionId: groupingOrCipher.Grouping.Id, + groupingName: groupingOrCipher.Grouping.Name); + } + + await Navigation.PushAsync(page); } - else + else if(groupingOrCipher.Cipher != null) { - page = new VaultListCiphersPage(collectionId: grouping.Id, groupingName: grouping.Name); + var page = new VaultViewCipherPage(groupingOrCipher.Cipher.Type, groupingOrCipher.Cipher.Id); + await Navigation.PushForDeviceAsync(page); } - await Navigation.PushAsync(page); ((ListView)sender).SelectedItem = null; } @@ -283,5 +307,23 @@ namespace Bit.App.Pages Icon = "search.png"; } } + + public class GroupingOrCipherDataTemplateSelector : DataTemplateSelector + { + public GroupingOrCipherDataTemplateSelector(VaultListGroupingsPage page) + { + GroupingTemplate = new DataTemplate(() => new VaultGroupingViewCell()); + CipherTemplate = new DataTemplate(() => new VaultListViewCell( + (Cipher c) => Helpers.CipherMoreClickedAsync(page, c, false), true)); + } + + public DataTemplate GroupingTemplate { get; set; } + public DataTemplate CipherTemplate { get; set; } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + return ((GroupingOrCipher)item).Cipher == null ? GroupingTemplate : CipherTemplate; + } + } } }