diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index f8a4d826d..8cfa1477a 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -1075,6 +1075,21 @@ + + + + + + + + + + + + + + + diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index fe884130e..800f5d4a5 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -195,6 +195,7 @@ namespace Bit.Android container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); diff --git a/src/Android/Resources/Resource.Designer.cs b/src/Android/Resources/Resource.Designer.cs index f9f5f4a6f..ac08f7a93 100644 --- a/src/Android/Resources/Resource.Designer.cs +++ b/src/Android/Resources/Resource.Designer.cs @@ -2464,391 +2464,394 @@ namespace Bit.Android public const int common_plus_signin_btn_text_light_pressed = 2130837633; // aapt resource value: 0x7f020082 - public const int design_fab_background = 2130837634; + public const int cube = 2130837634; // aapt resource value: 0x7f020083 - public const int design_snackbar_background = 2130837635; + public const int design_fab_background = 2130837635; // aapt resource value: 0x7f020084 - public const int download = 2130837636; + public const int design_snackbar_background = 2130837636; // aapt resource value: 0x7f020085 - public const int envelope = 2130837637; + public const int download = 2130837637; // aapt resource value: 0x7f020086 - public const int eye = 2130837638; + public const int envelope = 2130837638; // aapt resource value: 0x7f020087 - public const int eye_slash = 2130837639; + public const int eye = 2130837639; // aapt resource value: 0x7f020088 - public const int fa_lock = 2130837640; + public const int eye_slash = 2130837640; // aapt resource value: 0x7f020089 - public const int fa_lock_selected = 2130837641; + public const int fa_lock = 2130837641; // aapt resource value: 0x7f02008a - public const int fingerprint = 2130837642; + public const int fa_lock_selected = 2130837642; // aapt resource value: 0x7f02008b - public const int fingerprint_white = 2130837643; + public const int fingerprint = 2130837643; // aapt resource value: 0x7f02008c - public const int folder = 2130837644; + public const int fingerprint_white = 2130837644; // aapt resource value: 0x7f02008d - public const int globe = 2130837645; + public const int folder = 2130837645; // aapt resource value: 0x7f02008e - public const int hockeyapp_btn_background = 2130837646; + public const int globe = 2130837646; // aapt resource value: 0x7f02008f - public const int ic_audiotrack = 2130837647; + public const int hockeyapp_btn_background = 2130837647; // aapt resource value: 0x7f020090 - public const int ic_audiotrack_light = 2130837648; + public const int ic_audiotrack = 2130837648; // aapt resource value: 0x7f020091 - public const int ic_bluetooth_grey = 2130837649; + public const int ic_audiotrack_light = 2130837649; // aapt resource value: 0x7f020092 - public const int ic_bluetooth_white = 2130837650; + public const int ic_bluetooth_grey = 2130837650; // aapt resource value: 0x7f020093 - public const int ic_cast_dark = 2130837651; + public const int ic_bluetooth_white = 2130837651; // aapt resource value: 0x7f020094 - public const int ic_cast_disabled_light = 2130837652; + public const int ic_cast_dark = 2130837652; // aapt resource value: 0x7f020095 - public const int ic_cast_grey = 2130837653; + public const int ic_cast_disabled_light = 2130837653; // aapt resource value: 0x7f020096 - public const int ic_cast_light = 2130837654; + public const int ic_cast_grey = 2130837654; // aapt resource value: 0x7f020097 - public const int ic_cast_off_light = 2130837655; + public const int ic_cast_light = 2130837655; // aapt resource value: 0x7f020098 - public const int ic_cast_on_0_light = 2130837656; + public const int ic_cast_off_light = 2130837656; // aapt resource value: 0x7f020099 - public const int ic_cast_on_1_light = 2130837657; + public const int ic_cast_on_0_light = 2130837657; // aapt resource value: 0x7f02009a - public const int ic_cast_on_2_light = 2130837658; + public const int ic_cast_on_1_light = 2130837658; // aapt resource value: 0x7f02009b - public const int ic_cast_on_light = 2130837659; + public const int ic_cast_on_2_light = 2130837659; // aapt resource value: 0x7f02009c - public const int ic_cast_white = 2130837660; + public const int ic_cast_on_light = 2130837660; // aapt resource value: 0x7f02009d - public const int ic_close_dark = 2130837661; + public const int ic_cast_white = 2130837661; // aapt resource value: 0x7f02009e - public const int ic_close_light = 2130837662; + public const int ic_close_dark = 2130837662; // aapt resource value: 0x7f02009f - public const int ic_collapse = 2130837663; + public const int ic_close_light = 2130837663; // aapt resource value: 0x7f0200a0 - public const int ic_collapse_00000 = 2130837664; + public const int ic_collapse = 2130837664; // aapt resource value: 0x7f0200a1 - public const int ic_collapse_00001 = 2130837665; + public const int ic_collapse_00000 = 2130837665; // aapt resource value: 0x7f0200a2 - public const int ic_collapse_00002 = 2130837666; + public const int ic_collapse_00001 = 2130837666; // aapt resource value: 0x7f0200a3 - public const int ic_collapse_00003 = 2130837667; + public const int ic_collapse_00002 = 2130837667; // aapt resource value: 0x7f0200a4 - public const int ic_collapse_00004 = 2130837668; + public const int ic_collapse_00003 = 2130837668; // aapt resource value: 0x7f0200a5 - public const int ic_collapse_00005 = 2130837669; + public const int ic_collapse_00004 = 2130837669; // aapt resource value: 0x7f0200a6 - public const int ic_collapse_00006 = 2130837670; + public const int ic_collapse_00005 = 2130837670; // aapt resource value: 0x7f0200a7 - public const int ic_collapse_00007 = 2130837671; + public const int ic_collapse_00006 = 2130837671; // aapt resource value: 0x7f0200a8 - public const int ic_collapse_00008 = 2130837672; + public const int ic_collapse_00007 = 2130837672; // aapt resource value: 0x7f0200a9 - public const int ic_collapse_00009 = 2130837673; + public const int ic_collapse_00008 = 2130837673; // aapt resource value: 0x7f0200aa - public const int ic_collapse_00010 = 2130837674; + public const int ic_collapse_00009 = 2130837674; // aapt resource value: 0x7f0200ab - public const int ic_collapse_00011 = 2130837675; + public const int ic_collapse_00010 = 2130837675; // aapt resource value: 0x7f0200ac - public const int ic_collapse_00012 = 2130837676; + public const int ic_collapse_00011 = 2130837676; // aapt resource value: 0x7f0200ad - public const int ic_collapse_00013 = 2130837677; + public const int ic_collapse_00012 = 2130837677; // aapt resource value: 0x7f0200ae - public const int ic_collapse_00014 = 2130837678; + public const int ic_collapse_00013 = 2130837678; // aapt resource value: 0x7f0200af - public const int ic_collapse_00015 = 2130837679; + public const int ic_collapse_00014 = 2130837679; // aapt resource value: 0x7f0200b0 - public const int ic_errorstatus = 2130837680; + public const int ic_collapse_00015 = 2130837680; // aapt resource value: 0x7f0200b1 - public const int ic_expand = 2130837681; + public const int ic_errorstatus = 2130837681; // aapt resource value: 0x7f0200b2 - public const int ic_expand_00000 = 2130837682; + public const int ic_expand = 2130837682; // aapt resource value: 0x7f0200b3 - public const int ic_expand_00001 = 2130837683; + public const int ic_expand_00000 = 2130837683; // aapt resource value: 0x7f0200b4 - public const int ic_expand_00002 = 2130837684; + public const int ic_expand_00001 = 2130837684; // aapt resource value: 0x7f0200b5 - public const int ic_expand_00003 = 2130837685; + public const int ic_expand_00002 = 2130837685; // aapt resource value: 0x7f0200b6 - public const int ic_expand_00004 = 2130837686; + public const int ic_expand_00003 = 2130837686; // aapt resource value: 0x7f0200b7 - public const int ic_expand_00005 = 2130837687; + public const int ic_expand_00004 = 2130837687; // aapt resource value: 0x7f0200b8 - public const int ic_expand_00006 = 2130837688; + public const int ic_expand_00005 = 2130837688; // aapt resource value: 0x7f0200b9 - public const int ic_expand_00007 = 2130837689; + public const int ic_expand_00006 = 2130837689; // aapt resource value: 0x7f0200ba - public const int ic_expand_00008 = 2130837690; + public const int ic_expand_00007 = 2130837690; // aapt resource value: 0x7f0200bb - public const int ic_expand_00009 = 2130837691; + public const int ic_expand_00008 = 2130837691; // aapt resource value: 0x7f0200bc - public const int ic_expand_00010 = 2130837692; + public const int ic_expand_00009 = 2130837692; // aapt resource value: 0x7f0200bd - public const int ic_expand_00011 = 2130837693; + public const int ic_expand_00010 = 2130837693; // aapt resource value: 0x7f0200be - public const int ic_expand_00012 = 2130837694; + public const int ic_expand_00011 = 2130837694; // aapt resource value: 0x7f0200bf - public const int ic_expand_00013 = 2130837695; + public const int ic_expand_00012 = 2130837695; // aapt resource value: 0x7f0200c0 - public const int ic_expand_00014 = 2130837696; + public const int ic_expand_00013 = 2130837696; // aapt resource value: 0x7f0200c1 - public const int ic_expand_00015 = 2130837697; + public const int ic_expand_00014 = 2130837697; // aapt resource value: 0x7f0200c2 - public const int ic_media_pause = 2130837698; + public const int ic_expand_00015 = 2130837698; // aapt resource value: 0x7f0200c3 - public const int ic_media_play = 2130837699; + public const int ic_media_pause = 2130837699; // aapt resource value: 0x7f0200c4 - public const int ic_media_route_disabled_mono_dark = 2130837700; + public const int ic_media_play = 2130837700; // aapt resource value: 0x7f0200c5 - public const int ic_media_route_off_mono_dark = 2130837701; + public const int ic_media_route_disabled_mono_dark = 2130837701; // aapt resource value: 0x7f0200c6 - public const int ic_media_route_on_0_mono_dark = 2130837702; + public const int ic_media_route_off_mono_dark = 2130837702; // aapt resource value: 0x7f0200c7 - public const int ic_media_route_on_1_mono_dark = 2130837703; + public const int ic_media_route_on_0_mono_dark = 2130837703; // aapt resource value: 0x7f0200c8 - public const int ic_media_route_on_2_mono_dark = 2130837704; + public const int ic_media_route_on_1_mono_dark = 2130837704; // aapt resource value: 0x7f0200c9 - public const int ic_media_route_on_mono_dark = 2130837705; + public const int ic_media_route_on_2_mono_dark = 2130837705; // aapt resource value: 0x7f0200ca - public const int ic_pause_dark = 2130837706; + public const int ic_media_route_on_mono_dark = 2130837706; // aapt resource value: 0x7f0200cb - public const int ic_pause_light = 2130837707; + public const int ic_pause_dark = 2130837707; // aapt resource value: 0x7f0200cc - public const int ic_play_dark = 2130837708; + public const int ic_pause_light = 2130837708; // aapt resource value: 0x7f0200cd - public const int ic_play_light = 2130837709; + public const int ic_play_dark = 2130837709; // aapt resource value: 0x7f0200ce - public const int ic_speaker_dark = 2130837710; + public const int ic_play_light = 2130837710; // aapt resource value: 0x7f0200cf - public const int ic_speaker_group_dark = 2130837711; + public const int ic_speaker_dark = 2130837711; // aapt resource value: 0x7f0200d0 - public const int ic_speaker_group_light = 2130837712; + public const int ic_speaker_group_dark = 2130837712; // aapt resource value: 0x7f0200d1 - public const int ic_speaker_light = 2130837713; + public const int ic_speaker_group_light = 2130837713; // aapt resource value: 0x7f0200d2 - public const int ic_successstatus = 2130837714; + public const int ic_speaker_light = 2130837714; // aapt resource value: 0x7f0200d3 - public const int ic_tv_dark = 2130837715; + public const int ic_successstatus = 2130837715; // aapt resource value: 0x7f0200d4 - public const int ic_tv_light = 2130837716; + public const int ic_tv_dark = 2130837716; // aapt resource value: 0x7f0200d5 - public const int icon = 2130837717; + public const int ic_tv_light = 2130837717; // aapt resource value: 0x7f0200d6 - public const int id = 2130837718; + public const int icon = 2130837718; // aapt resource value: 0x7f0200d7 - public const int ion_chevron_right = 2130837719; + public const int id = 2130837719; // aapt resource value: 0x7f0200d8 - public const int launch = 2130837720; + public const int ion_chevron_right = 2130837720; // aapt resource value: 0x7f0200d9 - public const int lightbulb = 2130837721; + public const int launch = 2130837721; // aapt resource value: 0x7f0200da - public const int list_selector = 2130837722; + public const int lightbulb = 2130837722; // aapt resource value: 0x7f0200db - public const int @lock = 2130837723; + public const int list_selector = 2130837723; // aapt resource value: 0x7f0200dc - public const int login = 2130837724; + public const int @lock = 2130837724; // aapt resource value: 0x7f0200dd - public const int logo = 2130837725; + public const int login = 2130837725; // aapt resource value: 0x7f0200de - public const int more = 2130837726; + public const int logo = 2130837726; // aapt resource value: 0x7f0200df - public const int mr_dialog_material_background_dark = 2130837727; + public const int more = 2130837727; // aapt resource value: 0x7f0200e0 - public const int mr_dialog_material_background_light = 2130837728; + public const int mr_dialog_material_background_dark = 2130837728; // aapt resource value: 0x7f0200e1 - public const int mr_ic_audiotrack_light = 2130837729; + public const int mr_dialog_material_background_light = 2130837729; // aapt resource value: 0x7f0200e2 - public const int mr_ic_cast_dark = 2130837730; + public const int mr_ic_audiotrack_light = 2130837730; // aapt resource value: 0x7f0200e3 - public const int mr_ic_cast_light = 2130837731; + public const int mr_ic_cast_dark = 2130837731; // aapt resource value: 0x7f0200e4 - public const int mr_ic_close_dark = 2130837732; + public const int mr_ic_cast_light = 2130837732; // aapt resource value: 0x7f0200e5 - public const int mr_ic_close_light = 2130837733; + public const int mr_ic_close_dark = 2130837733; // aapt resource value: 0x7f0200e6 - public const int mr_ic_media_route_connecting_mono_dark = 2130837734; + public const int mr_ic_close_light = 2130837734; // aapt resource value: 0x7f0200e7 - public const int mr_ic_media_route_connecting_mono_light = 2130837735; + public const int mr_ic_media_route_connecting_mono_dark = 2130837735; // aapt resource value: 0x7f0200e8 - public const int mr_ic_media_route_mono_dark = 2130837736; + public const int mr_ic_media_route_connecting_mono_light = 2130837736; // aapt resource value: 0x7f0200e9 - public const int mr_ic_media_route_mono_light = 2130837737; + public const int mr_ic_media_route_mono_dark = 2130837737; // aapt resource value: 0x7f0200ea - public const int mr_ic_pause_dark = 2130837738; + public const int mr_ic_media_route_mono_light = 2130837738; // aapt resource value: 0x7f0200eb - public const int mr_ic_pause_light = 2130837739; + public const int mr_ic_pause_dark = 2130837739; // aapt resource value: 0x7f0200ec - public const int mr_ic_play_dark = 2130837740; + public const int mr_ic_pause_light = 2130837740; // aapt resource value: 0x7f0200ed - public const int mr_ic_play_light = 2130837741; + public const int mr_ic_play_dark = 2130837741; // aapt resource value: 0x7f0200ee - public const int note = 2130837742; + public const int mr_ic_play_light = 2130837742; // aapt resource value: 0x7f0200ef - public const int notification_sm = 2130837743; - - // aapt resource value: 0x7f020102 - public const int notification_template_icon_bg = 2130837762; + public const int note = 2130837743; // aapt resource value: 0x7f0200f0 - public const int paperclip = 2130837744; + public const int notification_sm = 2130837744; + + // aapt resource value: 0x7f020103 + public const int notification_template_icon_bg = 2130837763; // aapt resource value: 0x7f0200f1 - public const int plus = 2130837745; + public const int paperclip = 2130837745; // aapt resource value: 0x7f0200f2 - public const int refresh = 2130837746; + public const int plus = 2130837746; // aapt resource value: 0x7f0200f3 - public const int roundedbg = 2130837747; + public const int refresh = 2130837747; // aapt resource value: 0x7f0200f4 - public const int roundedbgdark = 2130837748; + public const int roundedbg = 2130837748; // aapt resource value: 0x7f0200f5 - public const int search = 2130837749; + public const int roundedbgdark = 2130837749; // aapt resource value: 0x7f0200f6 - public const int share = 2130837750; + public const int search = 2130837750; // aapt resource value: 0x7f0200f7 - public const int share_tools = 2130837751; + public const int share = 2130837751; // aapt resource value: 0x7f0200f8 - public const int shield = 2130837752; + public const int share_tools = 2130837752; // aapt resource value: 0x7f0200f9 - public const int splash_screen = 2130837753; + public const int shield = 2130837753; // aapt resource value: 0x7f0200fa - public const int star = 2130837754; + public const int splash_screen = 2130837754; // aapt resource value: 0x7f0200fb - public const int star_selected = 2130837755; + public const int star = 2130837755; // aapt resource value: 0x7f0200fc - public const int tools = 2130837756; + public const int star_selected = 2130837756; // aapt resource value: 0x7f0200fd - public const int tools_selected = 2130837757; + public const int tools = 2130837757; // aapt resource value: 0x7f0200fe - public const int trash = 2130837758; + public const int tools_selected = 2130837758; // aapt resource value: 0x7f0200ff - public const int upload = 2130837759; + public const int trash = 2130837759; // aapt resource value: 0x7f020100 - public const int user = 2130837760; + public const int upload = 2130837760; // aapt resource value: 0x7f020101 - public const int yubikey = 2130837761; + public const int user = 2130837761; + + // aapt resource value: 0x7f020102 + public const int yubikey = 2130837762; static Drawable() { diff --git a/src/Android/Resources/drawable-hdpi/cube.png b/src/Android/Resources/drawable-hdpi/cube.png new file mode 100644 index 000000000..d28625178 Binary files /dev/null and b/src/Android/Resources/drawable-hdpi/cube.png differ diff --git a/src/Android/Resources/drawable-xhdpi/cube.png b/src/Android/Resources/drawable-xhdpi/cube.png new file mode 100644 index 000000000..eb6b71b24 Binary files /dev/null and b/src/Android/Resources/drawable-xhdpi/cube.png differ diff --git a/src/Android/Resources/drawable-xxhdpi/cube.png b/src/Android/Resources/drawable-xxhdpi/cube.png new file mode 100644 index 000000000..e66e6771d Binary files /dev/null and b/src/Android/Resources/drawable-xxhdpi/cube.png differ diff --git a/src/Android/Resources/drawable-xxxhdpi/cube.png b/src/Android/Resources/drawable-xxxhdpi/cube.png new file mode 100644 index 000000000..16f54cd34 Binary files /dev/null and b/src/Android/Resources/drawable-xxxhdpi/cube.png differ diff --git a/src/Android/Resources/drawable/cube.png b/src/Android/Resources/drawable/cube.png new file mode 100644 index 000000000..722d6b177 Binary files /dev/null and b/src/Android/Resources/drawable/cube.png differ diff --git a/src/App/Abstractions/Services/ICollectionService.cs b/src/App/Abstractions/Services/ICollectionService.cs new file mode 100644 index 000000000..447d7d53d --- /dev/null +++ b/src/App/Abstractions/Services/ICollectionService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models; +using System; + +namespace Bit.App.Abstractions +{ + public interface ICollectionService + { + Task GetByIdAsync(string id); + Task> GetAllAsync(); + Task>> GetAllCipherAssociationsAsync(); + } +} diff --git a/src/App/App.csproj b/src/App/App.csproj index 3e68389cc..b2e8ed94c 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -45,6 +45,7 @@ + @@ -75,6 +76,7 @@ + @@ -90,6 +92,7 @@ + @@ -189,6 +192,7 @@ + @@ -350,6 +354,7 @@ AppResources.zh-Hant.resx + diff --git a/src/App/Controls/SectionHeaderViewCell.cs b/src/App/Controls/SectionHeaderViewCell.cs new file mode 100644 index 000000000..ddae064b3 --- /dev/null +++ b/src/App/Controls/SectionHeaderViewCell.cs @@ -0,0 +1,43 @@ +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class SectionHeaderViewCell : ExtendedViewCell + { + public SectionHeaderViewCell(string bindingName, string countBindingName = null, Thickness? padding = null) + { + var label = new Label + { + FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), + Style = (Style)Application.Current.Resources["text-muted"], + VerticalTextAlignment = TextAlignment.Center, + HorizontalOptions = LayoutOptions.StartAndExpand + }; + + label.SetBinding(Label.TextProperty, bindingName); + + var stackLayout = new StackLayout + { + Padding = padding ?? new Thickness(16, 8, 0, 8), + Children = { label }, + Orientation = StackOrientation.Horizontal + }; + + if(!string.IsNullOrWhiteSpace(countBindingName)) + { + var countLabel = new Label + { + LineBreakMode = LineBreakMode.NoWrap, + FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), + Style = (Style)Application.Current.Resources["text-muted"], + HorizontalOptions = LayoutOptions.End + }; + countLabel.SetBinding(Label.TextProperty, countBindingName); + stackLayout.Children.Add(countLabel); + } + + View = stackLayout; + BackgroundColor = Color.FromHex("efeff4"); + } + } +} diff --git a/src/App/Controls/VaultGroupingViewCell.cs b/src/App/Controls/VaultGroupingViewCell.cs new file mode 100644 index 000000000..dc9964c66 --- /dev/null +++ b/src/App/Controls/VaultGroupingViewCell.cs @@ -0,0 +1,79 @@ +using Bit.App.Models.Page; +using FFImageLoading.Forms; +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class VaultGroupingViewCell : ExtendedViewCell + { + public static readonly BindableProperty GroupingParameterProeprty = BindableProperty.Create(nameof(GroupingParameter), + typeof(VaultListPageModel.Grouping), typeof(VaultGroupingViewCell), null); + + public VaultGroupingViewCell() + { + Icon = new CachedImage + { + WidthRequest = 20, + HeightRequest = 20, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + Source = "folder.png", + Margin = new Thickness(0, 0, 10, 0) + }; + + Label = new Label + { + LineBreakMode = LineBreakMode.TailTruncation, + FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), + HorizontalOptions = LayoutOptions.StartAndExpand + }; + Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Grouping.Name)); + + CountLabel = new Label + { + LineBreakMode = LineBreakMode.NoWrap, + FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), + Style = (Style)Application.Current.Resources["text-muted"], + HorizontalOptions = LayoutOptions.End + }; + CountLabel.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Grouping.CipherCount)); + + var stackLayout = new StackLayout + { + Spacing = 0, + Padding = new Thickness(16, 8), + Children = { Icon, Label, CountLabel }, + Orientation = StackOrientation.Horizontal + }; + + if(Device.RuntimePlatform == Device.Android) + { + Label.TextColor = Color.Black; + } + + View = stackLayout; + BackgroundColor = Color.White; + SetBinding(GroupingParameterProeprty, new Binding(".")); + } + + public VaultListPageModel.Grouping GroupingParameter + { + get => GetValue(GroupingParameterProeprty) as VaultListPageModel.Grouping; + set { SetValue(GroupingParameterProeprty, value); } + } + public CachedImage Icon { get; private set; } + public Label Label { get; private set; } + public Label CountLabel { get; private set; } + + protected override void OnBindingContextChanged() + { + if(BindingContext is VaultListPageModel.Grouping grouping) + { + Icon.Source = grouping.Folder ? "folder.png" : "cube.png"; + } + + base.OnBindingContextChanged(); + } + } +} diff --git a/src/App/Controls/VaultListViewCell.cs b/src/App/Controls/VaultListViewCell.cs index ced38c8bf..44ae58ec3 100644 --- a/src/App/Controls/VaultListViewCell.cs +++ b/src/App/Controls/VaultListViewCell.cs @@ -1,5 +1,4 @@ using Bit.App.Models.Page; -using FFImageLoading.Forms; using System; using Xamarin.Forms; diff --git a/src/App/Models/Page/VaultListPageModel.cs b/src/App/Models/Page/VaultListPageModel.cs index 8b4c77590..eb37ebac7 100644 --- a/src/App/Models/Page/VaultListPageModel.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -165,6 +165,52 @@ namespace Bit.App.Models.Page public string Name { get; set; } = AppResources.FolderNone; } + public class Section : List + { + public Section(List groupings, string name) + { + AddRange(groupings); + Name = name.ToUpperInvariant(); + ItemCount = groupings.Count; + } + + public string Name { get; set; } + public int ItemCount { get; set; } + } + + public class Grouping + { + public Grouping(string name, int count) + { + Id = null; + Name = name; + Folder = true; + CipherCount = count; + } + + public Grouping(Models.Folder folder, int count) + { + Id = folder.Id; + Name = folder.Name?.Decrypt(); + Folder = true; + CipherCount = count; + } + + public Grouping(Collection collection, int count) + { + Id = collection.Id; + Name = collection.Name?.Decrypt(collection.OrganizationId); + Collection = true; + CipherCount = count; + } + + public string Id { get; set; } + public string Name { get; set; } = AppResources.FolderNone; + public int CipherCount { get; set; } + public bool Folder { get; set; } + public bool Collection { get; set; } + } + public class AutofillGrouping : List { public AutofillGrouping(List logins, string name) diff --git a/src/App/Pages/MainPage.cs b/src/App/Pages/MainPage.cs index e292ddd33..76169c95b 100644 --- a/src/App/Pages/MainPage.cs +++ b/src/App/Pages/MainPage.cs @@ -13,7 +13,7 @@ namespace Bit.App.Pages var settingsNavigation = new ExtendedNavigationPage(new SettingsPage()); var favoritesNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(true)); - var vaultNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(false)); + var vaultNavigation = new ExtendedNavigationPage(new VaultListGroupingsPage()); var toolsNavigation = new ExtendedNavigationPage(new ToolsPage()); favoritesNavigation.Icon = "star.png"; diff --git a/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs b/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs index bd4b1015a..579fcf2c4 100644 --- a/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs +++ b/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs @@ -99,7 +99,8 @@ namespace Bit.App.Pages IsGroupingEnabled = true, ItemsSource = PresentationCiphersGroup, HasUnevenRows = true, - GroupHeaderTemplate = new DataTemplate(() => new HeaderViewCell()), + GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( + nameof(VaultListPageModel.AutofillGrouping.Name))), ItemTemplate = new DataTemplate(() => new VaultListViewCell( (VaultListPageModel.Cipher l) => MoreClickedAsync(l))) }; @@ -359,29 +360,5 @@ namespace Bit.App.Pages TimeSpan.FromSeconds(10)); } } - - private class HeaderViewCell : ExtendedViewCell - { - public HeaderViewCell() - { - var label = new Label - { - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - VerticalTextAlignment = TextAlignment.Center - }; - - label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.AutofillGrouping.Name)); - - var grid = new ContentView - { - Padding = new Thickness(16, 8, 0, 8), - Content = label - }; - - View = grid; - BackgroundColor = Color.FromHex("efeff4"); - } - } } } diff --git a/src/App/Pages/Vault/VaultListGroupingsPage.cs b/src/App/Pages/Vault/VaultListGroupingsPage.cs new file mode 100644 index 000000000..ad775c4ab --- /dev/null +++ b/src/App/Pages/Vault/VaultListGroupingsPage.cs @@ -0,0 +1,296 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Acr.UserDialogs; +using Bit.App.Abstractions; +using Bit.App.Controls; +using Bit.App.Models.Page; +using Bit.App.Resources; +using Xamarin.Forms; +using XLabs.Ioc; +using Bit.App.Utilities; +using Plugin.Settings.Abstractions; +using Plugin.Connectivity.Abstractions; +using System.Collections.Generic; +using System.Threading; +using Bit.App.Enums; + +namespace Bit.App.Pages +{ + public class VaultListGroupingsPage : ExtendedContentPage + { + private readonly IFolderService _folderService; + private readonly ICollectionService _collectionService; + private readonly ICipherService _cipherService; + private readonly IUserDialogs _userDialogs; + private readonly IConnectivity _connectivity; + private readonly IDeviceActionService _deviceActionService; + private readonly ISyncService _syncService; + private readonly IPushNotificationService _pushNotification; + private readonly IDeviceInfoService _deviceInfoService; + private readonly ISettings _settings; + private readonly IAppSettingsService _appSettingsService; + private readonly IGoogleAnalyticsService _googleAnalyticsService; + private CancellationTokenSource _filterResultsCancellationTokenSource; + + public VaultListGroupingsPage() + : base(true) + { + _folderService = Resolver.Resolve(); + _collectionService = Resolver.Resolve(); + _cipherService = Resolver.Resolve(); + _connectivity = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); + _deviceActionService = Resolver.Resolve(); + _syncService = Resolver.Resolve(); + _pushNotification = Resolver.Resolve(); + _deviceInfoService = Resolver.Resolve(); + _settings = Resolver.Resolve(); + _appSettingsService = Resolver.Resolve(); + _googleAnalyticsService = Resolver.Resolve(); + + Init(); + } + + public ExtendedObservableCollection PresentationSections { get; private set; } + = new ExtendedObservableCollection(); + public ListView ListView { get; set; } + public SearchBar Search { get; set; } + public StackLayout NoDataStackLayout { get; set; } + public StackLayout ResultsStackLayout { get; set; } + public ActivityIndicator LoadingIndicator { get; set; } + private AddCipherToolBarItem AddCipherItem { get; set; } + + private void Init() + { + AddCipherItem = new AddCipherToolBarItem(this); + ToolbarItems.Add(AddCipherItem); + + ListView = new ListView(ListViewCachingStrategy.RecycleElement) + { + IsGroupingEnabled = true, + ItemsSource = PresentationSections, + HasUnevenRows = true, + GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell( + nameof(VaultListPageModel.Section.Name), nameof(VaultListPageModel.Section.ItemCount), + new Thickness(16, Helpers.OnPlatform(20, 12, 12), 16, 12))), + ItemTemplate = new DataTemplate(() => new VaultGroupingViewCell()) + }; + + if(Device.RuntimePlatform == Device.iOS) + { + ListView.RowHeight = -1; + } + + Search = new SearchBar + { + Placeholder = AppResources.SearchVault, + FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Button)), + CancelButtonColor = Color.FromHex("3c8dbc") + }; + // Bug with searchbar on android 7, ref https://bugzilla.xamarin.com/show_bug.cgi?id=43975 + if(Device.RuntimePlatform == Device.Android && _deviceInfoService.Version >= 24) + { + Search.HeightRequest = 50; + } + + Title = AppResources.MyVault; + + ResultsStackLayout = new StackLayout + { + Children = { Search, ListView }, + Spacing = 0 + }; + + var noDataLabel = new Label + { + Text = AppResources.NoItems, + HorizontalTextAlignment = TextAlignment.Center, + FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), + Style = (Style)Application.Current.Resources["text-muted"] + }; + + NoDataStackLayout = new StackLayout + { + Children = { noDataLabel }, + VerticalOptions = LayoutOptions.CenterAndExpand, + Padding = new Thickness(20, 0), + Spacing = 20 + }; + + var addCipherButton = new ExtendedButton + { + Text = AppResources.AddAnItem, + Command = new Command(() => AddCipher()), + Style = (Style)Application.Current.Resources["btn-primaryAccent"] + }; + + NoDataStackLayout.Children.Add(addCipherButton); + + LoadingIndicator = new ActivityIndicator + { + IsRunning = true, + VerticalOptions = LayoutOptions.CenterAndExpand, + HorizontalOptions = LayoutOptions.Center + }; + + Content = LoadingIndicator; + } + + protected override void OnAppearing() + { + base.OnAppearing(); + MessagingCenter.Subscribe(_syncService, "SyncCompleted", (sender, success) => + { + if(success) + { + _filterResultsCancellationTokenSource = FetchAndLoadVault(); + } + }); + + ListView.ItemSelected += GroupingSelected; + //Search.TextChanged += SearchBar_TextChanged; + //Search.SearchButtonPressed += SearchBar_SearchButtonPressed; + AddCipherItem?.InitEvents(); + + _filterResultsCancellationTokenSource = FetchAndLoadVault(); + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + MessagingCenter.Unsubscribe(_syncService, "SyncCompleted"); + + ListView.ItemSelected -= GroupingSelected; + //Search.TextChanged -= SearchBar_TextChanged; + //Search.SearchButtonPressed -= SearchBar_SearchButtonPressed; + AddCipherItem?.Dispose(); + } + + private void AdjustContent() + { + if(PresentationSections.Count > 0 || !string.IsNullOrWhiteSpace(Search.Text)) + { + Content = ResultsStackLayout; + } + else + { + Content = NoDataStackLayout; + } + } + + private CancellationTokenSource FetchAndLoadVault() + { + var cts = new CancellationTokenSource(); + _filterResultsCancellationTokenSource?.Cancel(); + + Task.Run(async () => + { + var sections = new List(); + var ciphers = await _cipherService.GetAllAsync(); + var collectionsDict = (await _collectionService.GetAllCipherAssociationsAsync()) + .GroupBy(c => c.Item2).ToDictionary(g => g.Key, v => v.ToList()); + + var folderCounts = new Dictionary { ["none"] = 0 }; + foreach(var cipher in ciphers) + { + if(cipher.FolderId != null) + { + if(!folderCounts.ContainsKey(cipher.FolderId)) + { + folderCounts.Add(cipher.FolderId, 0); + } + folderCounts[cipher.FolderId]++; + } + else + { + folderCounts["none"]++; + } + } + + var folders = await _folderService.GetAllAsync(); + var folderGroupings = folders? + .Select(f => new VaultListPageModel.Grouping(f, folderCounts.ContainsKey(f.Id) ? folderCounts[f.Id] : 0)) + .OrderBy(g => g.Name).ToList(); + folderGroupings.Add(new VaultListPageModel.Grouping(AppResources.FolderNone, folderCounts["none"])); + if(folderGroupings?.Any() ?? false) + { + sections.Add(new VaultListPageModel.Section(folderGroupings, AppResources.Folders)); + } + + var collections = await _collectionService.GetAllAsync(); + var collectionGroupings = collections? + .Select(c => new VaultListPageModel.Grouping(c, + collectionsDict.ContainsKey(c.Id) ? collectionsDict[c.Id].Count() : 0)) + .OrderBy(g => g.Name).ToList(); + if(collectionGroupings?.Any() ?? false) + { + sections.Add(new VaultListPageModel.Section(collectionGroupings, AppResources.Collections)); + } + + Device.BeginInvokeOnMainThread(() => + { + if(sections.Any()) + { + PresentationSections.ResetWithRange(sections); + } + + AdjustContent(); + }); + }, cts.Token); + + return cts; + } + + private void GroupingSelected(object sender, SelectedItemChangedEventArgs e) + { + var grouping = e.SelectedItem as VaultListPageModel.Grouping; + if(grouping == null) + { + return; + } + + ((ListView)sender).SelectedItem = null; + } + + private async void AddCipher() + { + var type = await _userDialogs.ActionSheetAsync(AppResources.SelectTypeAdd, AppResources.Cancel, null, null, + AppResources.TypeLogin, AppResources.TypeCard, AppResources.TypeIdentity, AppResources.TypeSecureNote); + + var selectedType = CipherType.SecureNote; + if(type == AppResources.Cancel) + { + return; + } + else if(type == AppResources.TypeLogin) + { + selectedType = CipherType.Login; + } + else if(type == AppResources.TypeCard) + { + selectedType = CipherType.Card; + } + else if(type == AppResources.TypeIdentity) + { + selectedType = CipherType.Identity; + } + + var page = new VaultAddCipherPage(selectedType); + await Navigation.PushForDeviceAsync(page); + } + + private class AddCipherToolBarItem : ExtendedToolbarItem + { + private readonly VaultListGroupingsPage _page; + + public AddCipherToolBarItem(VaultListGroupingsPage page) + : base(() => page.AddCipher()) + { + _page = page; + Text = AppResources.Add; + Icon = "plus.png"; + } + } + } +} diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index e55e1566a..2d402ad43 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -673,6 +673,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Collections. + /// + public static string Collections { + get { + return ResourceManager.GetString("Collections", resourceCulture); + } + } + /// /// Looks up a localized string similar to Coming Soon!. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 2cd9e8e10..5494135b6 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1194,4 +1194,7 @@ Go to my vault + + Collections + \ No newline at end of file diff --git a/src/App/Services/CollectionService.cs b/src/App/Services/CollectionService.cs new file mode 100644 index 000000000..009c101d9 --- /dev/null +++ b/src/App/Services/CollectionService.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models; + +namespace Bit.App.Services +{ + public class CollectionService : ICollectionService + { + private readonly ICollectionRepository _collectionRepository; + private readonly ICipherCollectionRepository _cipherCollectionRepository; + private readonly IAuthService _authService; + + public CollectionService( + ICollectionRepository collectionRepository, + ICipherCollectionRepository cipherCollectionRepository, + IAuthService authService) + { + _collectionRepository = collectionRepository; + _cipherCollectionRepository = cipherCollectionRepository; + _authService = authService; + } + + public async Task GetByIdAsync(string id) + { + var data = await _collectionRepository.GetByIdAsync(id); + if(data == null || data.UserId != _authService.UserId) + { + return null; + } + + var collection = new Collection(data); + return collection; + } + + public async Task> GetAllAsync() + { + var data = await _collectionRepository.GetAllByUserIdAsync(_authService.UserId); + var collections = data.Select(c => new Collection(c)); + return collections; + } + + public async Task>> GetAllCipherAssociationsAsync() + { + var data = await _cipherCollectionRepository.GetAllByUserIdAsync(_authService.UserId); + var assocs = data.Select(cc => new Tuple(cc.CipherId, cc.CollectionId)); + return assocs; + } + } +} diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 9887d6511..535ed986a 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -277,6 +277,7 @@ namespace Bit.iOS.Extension container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 4ba1a64db..a89deb512 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -255,6 +255,7 @@ namespace Bit.iOS container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); diff --git a/src/iOS/Resources/cube.png b/src/iOS/Resources/cube.png new file mode 100644 index 000000000..722d6b177 Binary files /dev/null and b/src/iOS/Resources/cube.png differ diff --git a/src/iOS/Resources/cube@2x.png b/src/iOS/Resources/cube@2x.png new file mode 100644 index 000000000..eb6b71b24 Binary files /dev/null and b/src/iOS/Resources/cube@2x.png differ diff --git a/src/iOS/Resources/cube@3x.png b/src/iOS/Resources/cube@3x.png new file mode 100644 index 000000000..e66e6771d Binary files /dev/null and b/src/iOS/Resources/cube@3x.png differ diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index da4c6690c..488622220 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -817,6 +817,15 @@ + + + + + + + + +