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