mirror of
https://github.com/bitwarden/mobile
synced 2026-01-03 00:53:27 +00:00
PM-3349 PM-3350 MAUI Migration Initial
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ContentView
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:xct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:view="clr-namespace:Bit.Core.Models.View"
|
||||
x:Name="_mainOverlay"
|
||||
x:DataType="controls:AccountSwitchingOverlayViewModel"
|
||||
x:Class="Bit.App.Controls.AccountSwitchingOverlayView"
|
||||
BackgroundColor="#22000000"
|
||||
Padding="0"
|
||||
IsVisible="False">
|
||||
<StackLayout
|
||||
x:Name="_accountListContainer"
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Transparent">
|
||||
<Frame
|
||||
Padding="0"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="Start">
|
||||
<Frame.Shadow>
|
||||
<Shadow
|
||||
Brush="Black"
|
||||
Radius="10"
|
||||
Offset="0,3" />
|
||||
</Frame.Shadow>
|
||||
<ListView
|
||||
x:Name="_accountListView"
|
||||
ItemsSource="{Binding BindingContext.AccountViews, Source={x:Reference _mainOverlay}}"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}"
|
||||
VerticalOptions="Start"
|
||||
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
|
||||
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"
|
||||
AutomationId="AccountListView">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="view:AccountView">
|
||||
<controls:AccountViewCell
|
||||
Account="{Binding .}"
|
||||
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
|
||||
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
|
||||
AutomationId="AccountViewCell"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
<ListView.Effects>
|
||||
<effects:ScrollViewContentInsetAdjustmentBehaviorEffect />
|
||||
</ListView.Effects>
|
||||
</ListView>
|
||||
</Frame>
|
||||
<BoxView
|
||||
BackgroundColor="Transparent"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<BoxView.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="FreeSpaceOverlay_Tapped" />
|
||||
</BoxView.GestureRecognizers>
|
||||
</BoxView>
|
||||
</StackLayout>
|
||||
</ContentView>
|
||||
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class AccountSwitchingOverlayView : ContentView
|
||||
{
|
||||
public static readonly BindableProperty MainPageProperty = BindableProperty.Create(
|
||||
nameof(MainPage),
|
||||
typeof(ContentPage),
|
||||
typeof(AccountSwitchingOverlayView),
|
||||
defaultBindingMode: BindingMode.OneWay);
|
||||
|
||||
public static readonly BindableProperty MainFabProperty = BindableProperty.Create(
|
||||
nameof(MainFab),
|
||||
typeof(View),
|
||||
typeof(AccountSwitchingOverlayView),
|
||||
defaultBindingMode: BindingMode.OneWay);
|
||||
|
||||
public ContentPage MainPage
|
||||
{
|
||||
get => (ContentPage)GetValue(MainPageProperty);
|
||||
set => SetValue(MainPageProperty, value);
|
||||
}
|
||||
|
||||
public View MainFab
|
||||
{
|
||||
get => (View)GetValue(MainFabProperty);
|
||||
set => SetValue(MainFabProperty, value);
|
||||
}
|
||||
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
public AccountSwitchingOverlayView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync,
|
||||
onException: ex => _logger.Value.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
SelectAccountCommand = new AsyncCommand<AccountViewCellViewModel>(SelectAccountAsync,
|
||||
onException: ex => _logger.Value.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
LongPressAccountCommand = new AsyncCommand<AccountViewCellViewModel>(LongPressAccountAsync,
|
||||
onException: ex => _logger.Value.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel;
|
||||
|
||||
public ICommand ToggleVisibililtyCommand { get; }
|
||||
|
||||
public ICommand SelectAccountCommand { get; }
|
||||
|
||||
public ICommand LongPressAccountCommand { get; }
|
||||
|
||||
public int AccountListRowHeight => // 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
|
||||
Device.RuntimePlatform == Device.Android ? 74 : 70;
|
||||
|
||||
public bool LongPressAccountEnabled { get; set; } = true;
|
||||
|
||||
public Action AfterHide { get; set; }
|
||||
|
||||
public async Task ToggleVisibilityAsync()
|
||||
{
|
||||
if (IsVisible)
|
||||
{
|
||||
await HideAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowAsync()
|
||||
{
|
||||
if (ViewModel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ViewModel.RefreshAccountViewsAsync();
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
// start listView in default (off-screen) position
|
||||
await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 0);
|
||||
|
||||
// re-measure in case accounts have been removed without changing screens
|
||||
if (ViewModel.AccountViews != null)
|
||||
{
|
||||
_accountListView.HeightRequest = AccountListRowHeight * ViewModel.AccountViews.Count;
|
||||
}
|
||||
|
||||
// set overlay opacity to zero before making visible and start fade-in
|
||||
Opacity = 0;
|
||||
IsVisible = true;
|
||||
this.FadeTo(1, 100);
|
||||
|
||||
if (Device.RuntimePlatform == Device.Android && MainFab != null)
|
||||
{
|
||||
// start fab fade-out
|
||||
MainFab.FadeTo(0, 200);
|
||||
}
|
||||
|
||||
// slide account list into view
|
||||
await _accountListContainer.TranslateTo(0, 0, 200, Easing.SinOut);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task HideAsync()
|
||||
{
|
||||
if (!IsVisible)
|
||||
{
|
||||
// already hidden, don't animate again
|
||||
return;
|
||||
}
|
||||
// Not all animations are awaited. This is intentional to allow multiple simultaneous animations.
|
||||
await Device.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
// start overlay fade-out
|
||||
this.FadeTo(0, 200);
|
||||
|
||||
if (Device.RuntimePlatform == Device.Android && MainFab != null)
|
||||
{
|
||||
// start fab fade-in
|
||||
MainFab.FadeTo(1, 200);
|
||||
}
|
||||
|
||||
// slide account list out of view
|
||||
await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 200, Easing.SinIn);
|
||||
|
||||
// remove overlay
|
||||
IsVisible = false;
|
||||
|
||||
AfterHide?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private async void FreeSpaceOverlay_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HideAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await HideAsync();
|
||||
|
||||
ViewModel?.SelectAccountCommand?.Execute(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LongPressAccountAsync(AccountViewCellViewModel item)
|
||||
{
|
||||
if (!LongPressAccountEnabled || !item.IsAccount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await HideAsync();
|
||||
|
||||
ViewModel?.LongPressAccountCommand?.Execute(
|
||||
new Tuple<ContentPage, AccountViewCellViewModel>(MainPage, item));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class AccountSwitchingOverlayViewModel : ExtendedViewModel
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
public AccountSwitchingOverlayViewModel(IStateService stateService,
|
||||
IMessagingService messagingService,
|
||||
ILogger logger)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_messagingService = messagingService;
|
||||
|
||||
SelectAccountCommand = new AsyncCommand<AccountViewCellViewModel>(SelectAccountAsync,
|
||||
onException: ex => logger.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
LongPressAccountCommand = new AsyncCommand<Tuple<ContentPage, AccountViewCellViewModel>>(LongPressAccountAsync,
|
||||
onException: ex => logger.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
// this needs to be a new list every time for the binding to get updated,
|
||||
// XF doesn't currentlyl provide a direct way to update on same instance
|
||||
// https://github.com/xamarin/Xamarin.Forms/issues/1950
|
||||
public List<AccountView> AccountViews => _stateService?.AccountViews is null ? null : new List<AccountView>(_stateService.AccountViews);
|
||||
|
||||
public bool AllowActiveAccountSelection { get; set; }
|
||||
|
||||
public bool AllowAddAccountRow { get; set; }
|
||||
|
||||
public ICommand SelectAccountCommand { get; }
|
||||
|
||||
public ICommand LongPressAccountCommand { get; }
|
||||
|
||||
public bool FromIOSExtension { get; set; }
|
||||
|
||||
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
||||
{
|
||||
if (!item.AccountView.IsAccount)
|
||||
{
|
||||
_messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.AccountView.IsActive)
|
||||
{
|
||||
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
||||
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||
if (FromIOSExtension)
|
||||
{
|
||||
await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId);
|
||||
}
|
||||
}
|
||||
else if (AllowActiveAccountSelection)
|
||||
{
|
||||
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LongPressAccountAsync(Tuple<ContentPage, AccountViewCellViewModel> item)
|
||||
{
|
||||
var (page, account) = item;
|
||||
if (account.AccountView.IsAccount)
|
||||
{
|
||||
await AppHelpers.AccountListOptions(page, account);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshAccountViewsAsync()
|
||||
{
|
||||
await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow);
|
||||
|
||||
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(AccountViews)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user