PM-3349 PM-3350 Refactored cipher bindings to have a simpler approach reusing a new CipherItemViewModel to avoid unwanted issues in the app

This commit is contained in:
Federico Maccaroni 2023-12-07 23:59:06 -03:00
parent 19c393842f
commit 5803635f44
No known key found for this signature in database
GPG key ID: 5D233F8F2B034536
18 changed files with 119 additions and 329 deletions

View file

@ -1,68 +1,10 @@
using System; namespace Bit.App.Controls
using Bit.App.Pages;
using Bit.App.Utilities;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{ {
public partial class AuthenticatorViewCell : ExtendedGrid public partial class AuthenticatorViewCell : ExtendedGrid
{ {
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay);
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell));
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
public AuthenticatorViewCell() public AuthenticatorViewCell()
{ {
InitializeComponent(); InitializeComponent();
} }
public Command CopyCommand { get; set; }
public CipherView Cipher
{
get => GetValue(CipherProperty) as CipherView;
set => SetValue(CipherProperty, value);
}
public bool? WebsiteIconsEnabled
{
get => (bool)GetValue(WebsiteIconsEnabledProperty);
set => SetValue(WebsiteIconsEnabledProperty, value);
}
public long TotpSec
{
get => (long)GetValue(TotpSecProperty);
set => SetValue(TotpSecProperty, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled ?? false
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
private string _iconImageSource = string.Empty;
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
return _iconImageSource;
}
}
} }
} }

View file

@ -3,13 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.CipherViewCell" x:Class="Bit.App.Controls.CipherViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:ff="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Compat.Maui" xmlns:ff="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Compat.Maui"
xmlns:core="clr-namespace:Bit.Core" xmlns:core="clr-namespace:Bit.Core"
StyleClass="list-row, list-row-platform" StyleClass="list-row, list-row-platform"
RowSpacing="0" RowSpacing="0"
ColumnSpacing="0" ColumnSpacing="0"
x:DataType="controls:CipherViewCellViewModel" x:DataType="pages:CipherItemViewModel"
AutomationId="CipherCell"> AutomationId="CipherCell">
<Grid.Resources> <Grid.Resources>

View file

@ -1,7 +1,6 @@
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.Core.Models.View;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.App.Controls namespace Bit.App.Controls
@ -11,12 +10,6 @@ namespace Bit.App.Controls
private const int ICON_COLUMN_DEFAULT_WIDTH = 40; private const int ICON_COLUMN_DEFAULT_WIDTH = 40;
private const int ICON_IMAGE_DEFAULT_WIDTH = 22; private const int ICON_IMAGE_DEFAULT_WIDTH = 22;
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
nameof(Cipher), typeof(CipherView), typeof(CipherViewCell), default(CipherView), BindingMode.OneWay);
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell));
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create( public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell)); nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell));
@ -30,51 +23,17 @@ namespace Bit.App.Controls
_iconImage.HeightRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale; _iconImage.HeightRequest = ICON_IMAGE_DEFAULT_WIDTH * fontScale;
} }
public bool? WebsiteIconsEnabled
{
get => (bool)GetValue(WebsiteIconsEnabledProperty);
set => SetValue(WebsiteIconsEnabledProperty, value);
}
public CipherView Cipher
{
get => GetValue(CipherProperty) as CipherView;
set => SetValue(CipherProperty, value);
}
public ICommand ButtonCommand public ICommand ButtonCommand
{ {
get => GetValue(ButtonCommandProperty) as ICommand; get => GetValue(ButtonCommandProperty) as ICommand;
set => SetValue(ButtonCommandProperty, value); set => SetValue(ButtonCommandProperty, value);
} }
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (BindingContext is CipherViewCellViewModel cipherViewCellViewModel && propertyName == WebsiteIconsEnabledProperty.PropertyName)
{
cipherViewCellViewModel.WebsiteIconsEnabled = WebsiteIconsEnabled ?? false;
}
}
private void MoreButton_Clicked(object sender, EventArgs e) private void MoreButton_Clicked(object sender, EventArgs e)
{ {
// WORKAROUND: Added a temporary workaround so that the MoreButton still works in all pages even if it uses GroupingsPageListItem instead of CipherViewCellViewModel. if (BindingContext is CipherItemViewModel cipherItem)
// Ideally this should be fixed so that even Groupings Page uses CipherViewCellViewModel
CipherView cipherView = null;
if (BindingContext is CipherViewCellViewModel cipherViewCellViewModel)
{ {
cipherView = cipherViewCellViewModel.Cipher; ButtonCommand?.Execute(cipherItem.Cipher);
}
else if (BindingContext is GroupingsPageListItem groupingsPageListItem)
{
cipherView = groupingsPageListItem.Cipher;
}
if (cipherView != null)
{
ButtonCommand?.Execute(cipherView);
} }
} }
} }

View file

@ -1,66 +0,0 @@
using System.Globalization;
using Bit.App.Utilities;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class CipherViewToCipherViewCellViewModelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is CipherView cipher)
{
return new CipherViewCellViewModel(cipher, false);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
public class CipherViewCellViewModel : ExtendedViewModel
{
private CipherView _cipher;
private bool _websiteIconsEnabled;
private string _iconImageSource = string.Empty;
public CipherViewCellViewModel(CipherView cipherView, bool websiteIconsEnabled)
{
Cipher = cipherView;
WebsiteIconsEnabled = websiteIconsEnabled;
}
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value);
}
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
&& IconImageSource != null;
}
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
}
return _iconImageSource;
}
}
}
}

View file

@ -1,16 +1,11 @@
using System.Collections.Generic; using Bit.App.Models;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.Core.Resources.Localization;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -48,7 +43,7 @@ namespace Bit.App.Pages
var groupedItems = new List<GroupingsPageListGroup>(); var groupedItems = new List<GroupingsPageListGroup>();
var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null); var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null);
var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); var matching = ciphers.Item1?.Select(c => new CipherItemViewModel(c, WebsiteIconsEnabled)).ToList();
var hasMatching = matching?.Any() ?? false; var hasMatching = matching?.Any() ?? false;
if (matching?.Any() ?? false) if (matching?.Any() ?? false)
{ {
@ -57,7 +52,7 @@ namespace Bit.App.Pages
} }
var fuzzy = ciphers.Item2?.Select(c => var fuzzy = ciphers.Item2?.Select(c =>
new GroupingsPageListItem { Cipher = c, FuzzyAutofill = true }).ToList(); new CipherItemViewModel(c, WebsiteIconsEnabled, true)).ToList();
if (fuzzy?.Any() ?? false) if (fuzzy?.Any() ?? false)
{ {
groupedItems.Add( groupedItems.Add(
@ -70,7 +65,7 @@ namespace Bit.App.Pages
protected override async Task SelectCipherAsync(IGroupingsPageListItem item) protected override async Task SelectCipherAsync(IGroupingsPageListItem item)
{ {
if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null) if (!(item is CipherItemViewModel listItem) || listItem.Cipher is null)
{ {
return; return;
} }

View file

@ -0,0 +1,42 @@
using Bit.App.Utilities;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public class CipherItemViewModel : ExtendedViewModel, IGroupingsPageListItem
{
private readonly bool _websiteIconsEnabled;
private string _iconImageSource = string.Empty;
public CipherItemViewModel(CipherView cipherView, bool websiteIconsEnabled, bool fuzzyAutofill = false)
{
Cipher = cipherView;
_websiteIconsEnabled = websiteIconsEnabled;
FuzzyAutofill = fuzzyAutofill;
}
public CipherView Cipher { get; }
public bool FuzzyAutofill { get; }
public bool ShowIconImage
{
get => _websiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
&& IconImageSource != null;
}
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
}
return _iconImageSource;
}
}
}
}

View file

@ -28,7 +28,6 @@
<ResourceDictionary> <ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" /> <u:InverseBoolConverter x:Key="inverseBool" />
<controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" /> <controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" />
<controls:CipherViewToCipherViewCellViewModelConverter x:Key="cipherViewToCipherViewCellViewModel" />
<ToolbarItem <ToolbarItem
x:Name="_closeItem" x:Name="_closeItem"
@ -46,9 +45,7 @@
<DataTemplate x:Key="cipherTemplate" <DataTemplate x:Key="cipherTemplate"
x:DataType="pages:GroupingsPageListItem"> x:DataType="pages:GroupingsPageListItem">
<controls:CipherViewCell <controls:CipherViewCell
BindingContext="{Binding Cipher, Converter={StaticResource cipherViewToCipherViewCellViewModel}}" ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}" />
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
</DataTemplate> </DataTemplate>
<DataTemplate <DataTemplate

View file

@ -1,14 +1,10 @@
using System; using Bit.App.Abstractions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -37,12 +33,10 @@ namespace Bit.App.Pages
InitializeComponent(); InitializeComponent();
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes #if IOS
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_closeItem); ToolbarItems.Add(_closeItem);
ToolbarItems.Add(_addItem); ToolbarItems.Add(_addItem);
} #endif
SetActivityIndicator(_mainContent); SetActivityIndicator(_mainContent);
_vm = BindingContext as CipherSelectionPageViewModel; _vm = BindingContext as CipherSelectionPageViewModel;
@ -88,12 +82,12 @@ namespace Bit.App.Pages
{ {
if (message.Command == "syncStarted") if (message.Command == "syncStarted")
{ {
Device.BeginInvokeOnMainThread(() => IsBusy = true); MainThread.BeginInvokeOnMainThread(() => IsBusy = true);
} }
else if (message.Command == "syncCompleted") else if (message.Command == "syncCompleted")
{ {
await Task.Delay(500); await Task.Delay(500);
Device.BeginInvokeOnMainThread(() => MainThread.BeginInvokeOnMainThread(() =>
{ {
IsBusy = false; IsBusy = false;
if (_vm.LoadedOnce) if (_vm.LoadedOnce)
@ -130,11 +124,10 @@ namespace Bit.App.Pages
_accountListOverlay.HideAsync().FireAndForget(); _accountListOverlay.HideAsync().FireAndForget();
return true; return true;
} }
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
if (Device.RuntimePlatform == Device.Android) #if ANDROID
{
_appOptions.Uri = null; _appOptions.Uri = null;
} #endif
return base.OnBackButtonPressed(); return base.OnBackButtonPressed();
} }

View file

@ -18,8 +18,6 @@
<ContentPage.Resources> <ContentPage.Resources>
<ResourceDictionary> <ResourceDictionary>
<controls:CipherViewToCipherViewCellViewModelConverter x:Key="cipherViewToCipherViewCellViewModel" />
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" <ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" /> x:Name="_closeItem" x:Key="closeItem" />
<StackLayout <StackLayout
@ -55,7 +53,7 @@
IsVisible="{Binding ShowVaultFilter}" IsVisible="{Binding ShowVaultFilter}"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
Margin="0,5,0,0"> Margin="{OnPlatform Android='0,5,0,0', iOS='0,15'}">
<Label <Label
Text="{Binding VaultFilterDescription}" Text="{Binding VaultFilterDescription}"
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"
@ -112,10 +110,7 @@
<!--Binding context is not applied if the cell is the direct child, check for context https://github.com/dotnet/maui/issues/9131--> <!--Binding context is not applied if the cell is the direct child, check for context https://github.com/dotnet/maui/issues/9131-->
<Grid> <Grid>
<controls:CipherViewCell <controls:CipherViewCell
BindingContext="{Binding ., Converter={StaticResource cipherViewToCipherViewCellViewModel}}" ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}" />
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
/>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>

View file

@ -1,8 +1,8 @@
using Bit.App.Controls; using Bit.App.Controls;
using Bit.App.Models; using Bit.App.Models;
using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -120,9 +120,9 @@ namespace Bit.App.Pages
return; return;
} }
if (e.CurrentSelection?.FirstOrDefault() is CipherView cipher) if (e.CurrentSelection?.FirstOrDefault() is CipherItemViewModel cipherIteemVM)
{ {
await _vm.SelectCipherAsync(cipher); await _vm.SelectCipherAsync(cipherIteemVM.Cipher);
} }
} }

View file

@ -42,7 +42,7 @@ namespace Bit.App.Pages
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService"); _policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_logger = ServiceContainer.Resolve<ILogger>("logger"); _logger = ServiceContainer.Resolve<ILogger>("logger");
Ciphers = new ExtendedObservableCollection<CipherView>(); Ciphers = new ExtendedObservableCollection<CipherItemViewModel>();
CipherOptionsCommand = CreateDefaultAsyncRelayCommand<CipherView>(cipher => Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService), CipherOptionsCommand = CreateDefaultAsyncRelayCommand<CipherView>(cipher => Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService),
onException: ex => HandleException(ex), onException: ex => HandleException(ex),
allowsMultipleExecutions: false); allowsMultipleExecutions: false);
@ -53,7 +53,7 @@ namespace Bit.App.Pages
public ICommand CipherOptionsCommand { get; } public ICommand CipherOptionsCommand { get; }
public ICommand AddCipherCommand { get; } public ICommand AddCipherCommand { get; }
public ExtendedObservableCollection<CipherView> Ciphers { get; set; } public ExtendedObservableCollection<CipherItemViewModel> Ciphers { get; set; }
public Func<CipherView, bool> Filter { get; set; } public Func<CipherView, bool> Filter { get; set; }
public string AutofillUrl { get; set; } public string AutofillUrl { get; set; }
public bool Deleted { get; set; } public bool Deleted { get; set; }
@ -87,12 +87,6 @@ namespace Bit.App.Pages
public bool ShowAddCipher => ShowNoData && _appOptions?.OtpData != null; public bool ShowAddCipher => ShowNoData && _appOptions?.OtpData != null;
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
internal void Prepare(Func<CipherView, bool> filter, bool deleted, AppOptions appOptions) internal void Prepare(Func<CipherView, bool> filter, bool deleted, AppOptions appOptions)
{ {
Filter = filter; Filter = filter;
@ -105,7 +99,7 @@ namespace Bit.App.Pages
public async Task InitAsync() public async Task InitAsync()
{ {
await InitVaultFilterAsync(true); await InitVaultFilterAsync(true);
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); _websiteIconsEnabled = await _stateService.GetDisableFaviconAsync() != true;
PerformSearchIfPopulated(); PerformSearchIfPopulated();
} }
@ -157,7 +151,7 @@ namespace Bit.App.Pages
} }
MainThread.BeginInvokeOnMainThread(() => MainThread.BeginInvokeOnMainThread(() =>
{ {
Ciphers.ResetWithRange(ciphers); Ciphers.ResetWithRange(ciphers.Select(c => new CipherItemViewModel(c, _websiteIconsEnabled)).ToList());
ShowNoData = !shouldShowAllWhenEmpty && searchable && Ciphers.Count == 0; ShowNoData = !shouldShowAllWhenEmpty && searchable && Ciphers.Count == 0;
ShowList = (searchable || shouldShowAllWhenEmpty) && !ShowNoData; ShowList = (searchable || shouldShowAllWhenEmpty) && !ShowNoData;
}); });

View file

@ -32,8 +32,6 @@
<ContentPage.Resources> <ContentPage.Resources>
<ResourceDictionary> <ResourceDictionary>
<controls:CipherViewToCipherViewCellViewModelConverter x:Key="cipherViewToCipherViewCellViewModel" />
<ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}" <ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}"
Clicked="Sync_Clicked" Order="Secondary" /> Clicked="Sync_Clicked" Order="Secondary" />
<ToolbarItem x:Name="_lockItem" x:Key="lockItem" Text="{u:I18n Lock}" <ToolbarItem x:Name="_lockItem" x:Key="lockItem" Text="{u:I18n Lock}"
@ -45,19 +43,14 @@
SemanticProperties.Description="{u:I18n AddItem}" /> SemanticProperties.Description="{u:I18n AddItem}" />
<DataTemplate x:Key="cipherTemplate" <DataTemplate x:Key="cipherTemplate"
x:DataType="pages:GroupingsPageListItem"> x:DataType="pages:CipherItemViewModel">
<controls:CipherViewCell <controls:CipherViewCell
BindingContext="{Binding Cipher, Converter={StaticResource cipherViewToCipherViewCellViewModel}}" ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}" />
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="authenticatorTemplate" <DataTemplate x:Key="authenticatorTemplate"
x:DataType="pages:GroupingsPageTOTPListItem"> x:DataType="pages:GroupingsPageTOTPListItem">
<controls:AuthenticatorViewCell <controls:AuthenticatorViewCell />
Cipher="{Binding Cipher}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
TotpSec="{Binding TotpSec}" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="groupTemplate" <DataTemplate x:Key="groupTemplate"
@ -124,7 +117,7 @@
IsVisible="{Binding ShowVaultFilter}" IsVisible="{Binding ShowVaultFilter}"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
Margin="0,5,0,0"> Margin="{OnPlatform Android='0,5,0,0', iOS='0,15'}">
<Label <Label
Text="{Binding VaultFilterDescription}" Text="{Binding VaultFilterDescription}"
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"

View file

@ -215,13 +215,21 @@ namespace Bit.App.Pages
return; return;
} }
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem) var selection = e.CurrentSelection?.FirstOrDefault();
if (selection is GroupingsPageTOTPListItem totpItem)
{ {
await _vm.SelectCipherAsync(totpItem.Cipher); await _vm.SelectCipherAsync(totpItem.Cipher);
return; return;
} }
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item)) if (selection is CipherItemViewModel cipherItemVM)
{
await _vm.SelectCipherAsync(cipherItemVM.Cipher);
return;
}
if (!(selection is GroupingsPageListItem item))
{ {
return; return;
} }
@ -234,10 +242,6 @@ namespace Bit.App.Pages
{ {
await _vm.SelectTotpCodesAsync(); await _vm.SelectTotpCodesAsync();
} }
else if (item.Cipher != null)
{
await _vm.SelectCipherAsync(item.Cipher);
}
else if (item.Folder != null) else if (item.Folder != null)
{ {
await _vm.SelectFolderAsync(item.Folder); await _vm.SelectFolderAsync(item.Folder);

View file

@ -1,8 +1,8 @@
using Bit.Core.Resources.Localization; using Bit.App.Utilities.Automation;
using Bit.App.Utilities.Automation;
using Bit.Core; using Bit.Core;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
using CollectionView = Bit.Core.Models.View.CollectionView; using CollectionView = Bit.Core.Models.View.CollectionView;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -14,10 +14,8 @@ namespace Bit.App.Pages
public FolderView Folder { get; set; } public FolderView Folder { get; set; }
public CollectionView Collection { get; set; } public CollectionView Collection { get; set; }
public CipherView Cipher { get; set; }
public CipherType? Type { get; set; } public CipherType? Type { get; set; }
public string ItemCount { get; set; } public string ItemCount { get; set; }
public bool FuzzyAutofill { get; set; }
public bool IsTrash { get; set; } public bool IsTrash { get; set; }
public bool IsTotpCode { get; set; } public bool IsTotpCode { get; set; }

View file

@ -1,7 +1,4 @@
using Microsoft.Maui.Controls; namespace Bit.App.Pages
using Microsoft.Maui;
namespace Bit.App.Pages
{ {
public class GroupingsPageListItemSelector : DataTemplateSelector public class GroupingsPageListItemSelector : DataTemplateSelector
{ {
@ -12,19 +9,24 @@ namespace Bit.App.Pages
protected override DataTemplate OnSelectTemplate(object item, BindableObject container) protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{ {
if (item is GroupingsPageHeaderListItem)
{
return HeaderTemplate;
}
if (item is GroupingsPageTOTPListItem) if (item is GroupingsPageTOTPListItem)
{ {
return AuthenticatorTemplate; return AuthenticatorTemplate;
} }
if (item is GroupingsPageListItem listItem) if (item is CipherItemViewModel)
{ {
return listItem.Cipher != null ? CipherTemplate : GroupTemplate; return CipherTemplate;
}
if (item is GroupingsPageHeaderListItem)
{
return HeaderTemplate;
}
if (item is GroupingsPageListItem)
{
return GroupTemplate;
} }
return null; return null;

View file

@ -1,56 +1,34 @@
using System; using Bit.App.Utilities;
using System.Threading.Tasks;
using Bit.Core.Resources.Localization;
using Bit.App.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem public class GroupingsPageTOTPListItem : CipherItemViewModel, IGroupingsPageListItem
{ {
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService; private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _websiteIconsEnabled;
private string _iconImageSource = string.Empty;
private double _progress; private double _progress;
private string _totpSec; private string _totpSec;
private string _totpCodeFormatted; private string _totpCodeFormatted;
private TotpHelper _totpTickHelper; private readonly TotpHelper _totpTickHelper;
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled) public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
:base(cipherView, websiteIconsEnabled)
{ {
_totpService = ServiceContainer.Resolve<ITotpService>("totpService"); _clipboardService = ServiceContainer.Resolve<IClipboardService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
Cipher = cipherView;
WebsiteIconsEnabled = websiteIconsEnabled;
CopyCommand = CreateDefaultAsyncRelayCommand(CopyToClipboardAsync, CopyCommand = CreateDefaultAsyncRelayCommand(CopyToClipboardAsync,
onException: ex => _logger.Value.Exception(ex), onException: _logger.Value.Exception,
allowsMultipleExecutions: false); allowsMultipleExecutions: false);
_totpTickHelper = new TotpHelper(cipherView); _totpTickHelper = new TotpHelper(cipherView);
} }
public AsyncRelayCommand CopyCommand { get; set; } public AsyncRelayCommand CopyCommand { get; set; }
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value);
}
public string TotpCodeFormatted public string TotpCodeFormatted
{ {
get => _totpCodeFormatted; get => _totpCodeFormatted;
@ -72,31 +50,6 @@ namespace Bit.App.Pages
get => _progress; get => _progress;
set => SetProperty(ref _progress, value); set => SetProperty(ref _progress, value);
} }
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
return _iconImageSource;
}
}
public string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0]; public string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0];
@ -105,7 +58,7 @@ namespace Bit.App.Pages
public async Task CopyToClipboardAsync() public async Task CopyToClipboardAsync()
{ {
await _clipboardService.CopyTextAsync(TotpCodeFormatted?.Replace(" ", string.Empty)); await _clipboardService.CopyTextAsync(TotpCodeFormatted?.Replace(" ", string.Empty));
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp)); _platformUtilsService.Value.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
} }
public async Task TotpTickAsync() public async Task TotpTickAsync()

View file

@ -25,7 +25,6 @@ namespace Bit.App.Pages
private bool _websiteIconsEnabled; private bool _websiteIconsEnabled;
private bool _syncRefreshing; private bool _syncRefreshing;
private bool _showTotpFilter; private bool _showTotpFilter;
private bool _totpFilterEnable;
private string _noDataText; private string _noDataText;
private List<CipherView> _allCiphers; private List<CipherView> _allCiphers;
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>(); private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
@ -150,11 +149,6 @@ namespace Bit.App.Pages
get => _showList; get => _showList;
set => SetProperty(ref _showList, value); set => SetProperty(ref _showList, value);
} }
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowTotp public bool ShowTotp
{ {
get => _showTotpFilter; get => _showTotpFilter;
@ -206,7 +200,7 @@ namespace Bit.App.Pages
var groupedItems = new List<GroupingsPageListGroup>(); var groupedItems = new List<GroupingsPageListGroup>();
var page = Page as GroupingsPage; var page = Page as GroupingsPage;
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); _websiteIconsEnabled = await _stateService.GetDisableFaviconAsync() != true;
try try
{ {
await LoadDataAsync(); await LoadDataAsync();
@ -225,7 +219,7 @@ namespace Bit.App.Pages
var hasFavorites = FavoriteCiphers?.Any() ?? false; var hasFavorites = FavoriteCiphers?.Any() ?? false;
if (hasFavorites) if (hasFavorites)
{ {
var favListItems = FavoriteCiphers.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); var favListItems = FavoriteCiphers.Select(c => new CipherItemViewModel(c, _websiteIconsEnabled)).ToList();
groupedItems.Add(new GroupingsPageListGroup(favListItems, AppResources.Favorites, groupedItems.Add(new GroupingsPageListGroup(favListItems, AppResources.Favorites,
favListItems.Count, uppercaseGroupNames, true)); favListItems.Count, uppercaseGroupNames, true));
} }
@ -282,7 +276,7 @@ namespace Bit.App.Pages
if (ShowNoFolderCipherGroup) if (ShowNoFolderCipherGroup)
{ {
var noFolderCiphersListItems = NoFolderCiphers.Select( var noFolderCiphersListItems = NoFolderCiphers.Select(
c => new GroupingsPageListItem { Cipher = c }).ToList(); c => new CipherItemViewModel(c, _websiteIconsEnabled)).ToList();
groupedItems.Add(new GroupingsPageListGroup(noFolderCiphersListItems, AppResources.FolderNone, groupedItems.Add(new GroupingsPageListGroup(noFolderCiphersListItems, AppResources.FolderNone,
noFolderCiphersListItems.Count, uppercaseGroupNames, false)); noFolderCiphersListItems.Count, uppercaseGroupNames, false));
} }
@ -399,7 +393,7 @@ namespace Bit.App.Pages
_totpTickCts?.Cancel(); _totpTickCts?.Cancel();
if (ShowTotp) if (ShowTotp)
{ {
var ciphersListItems = TOTPCiphers.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList(); var ciphersListItems = TOTPCiphers.Select(c => new GroupingsPageTOTPListItem(c, _websiteIconsEnabled)).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
@ -408,7 +402,7 @@ namespace Bit.App.Pages
else else
{ {
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted) var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); .Select(c => new CipherItemViewModel(c, _websiteIconsEnabled)).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
} }

View file

@ -1,13 +1,7 @@
using System; using Bit.Core.Abstractions;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -42,7 +36,7 @@ namespace Bit.App.Pages
if (ciphers?.Any() ?? false) if (ciphers?.Any() ?? false)
{ {
groupedItems.Add( groupedItems.Add(
new GroupingsPageListGroup(ciphers.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(), new GroupingsPageListGroup(ciphers.Select(c => new CipherItemViewModel(c, WebsiteIconsEnabled)).ToList(),
AppResources.MatchingItems, AppResources.MatchingItems,
ciphers.Count, ciphers.Count,
false, false,
@ -54,7 +48,7 @@ namespace Bit.App.Pages
protected override async Task SelectCipherAsync(IGroupingsPageListItem item) protected override async Task SelectCipherAsync(IGroupingsPageListItem item)
{ {
if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null) if (!(item is CipherItemViewModel listItem) || listItem.Cipher is null)
{ {
return; return;
} }