1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-28 22:23:35 +00:00

PM-3349 PM-3350 MAUI Migration Initial

This commit is contained in:
Federico Maccaroni
2023-09-29 11:02:19 -03:00
parent bbef0f8c93
commit 8ef9443b1e
717 changed files with 5367 additions and 4702 deletions

View File

@@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
namespace Bit.App.Abstractions
{
public interface IAccountsManager
{
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction);
Task LogOutAsync(string userId, bool userInitiated, bool expired);
Task PromptToSwitchToExistingAccountAsync(string userId);
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.App.Abstractions
{
public interface INavigationParams { }
public interface IAccountsManagerHost
{
Task SetPreviousPageInfoAsync();
void Navigate(NavigationTarget navTarget, INavigationParams navParams = null);
Task UpdateThemeAsync();
}
}

View File

@@ -8,6 +8,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
using DeviceType = Bit.Core.Enums.DeviceType;
namespace Bit.Core.Abstractions
{

View File

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using CollectionView = Bit.Core.Models.View.CollectionView;
namespace Bit.Core.Abstractions
{

View File

@@ -0,0 +1,9 @@
using System;
namespace Bit.App.Abstractions
{
public interface IDeepLinkContext
{
bool OnNewUri(Uri uri);
}
}

View File

@@ -0,0 +1,50 @@
using System.Threading.Tasks;
using Bit.App.Utilities.Prompts;
using Bit.Core.Enums;
using Bit.Core.Models;
namespace Bit.App.Abstractions
{
public interface IDeviceActionService
{
string DeviceUserAgent { get; }
Core.Enums.DeviceType DeviceType { get; }
int SystemMajorVersion();
string SystemModel();
string GetBuildNumber();
void Toast(string text, bool longDuration = false);
Task ShowLoadingAsync(string text);
Task HideLoadingAsync();
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true, bool password = false);
Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config);
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
bool SupportsFaceBiometric();
Task<bool> SupportsFaceBiometricAsync();
bool SupportsNfc();
bool SupportsCamera();
bool SupportsFido2();
bool SupportsAutofillServices();
bool SupportsInlineAutofill();
bool SupportsDrawOver();
bool LaunchApp(string appName);
void RateApp();
void OpenAccessibilitySettings();
void OpenAccessibilityOverlayPermissionSettings();
void OpenAutofillSettings();
long GetActiveTime();
void CloseMainApp();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
void CloseExtensionPopUp();
string GetAutofillAccessibilityDescription();
string GetAutofillDrawOverDescription();
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Globalization;
namespace Bit.App.Abstractions
{
public interface ILocalizeService
{
CultureInfo GetCurrentCultureInfo();
/// <summary>
/// Format date using device locale.
/// Needed for iOS as it provides locales unsupported in .Net
/// </summary>
string GetLocaleShortDate(DateTime? date);
/// <summary>
/// Format time using device locale.
/// Needed for iOS as it provides locales unsupported in .Net
/// </summary>
string GetLocaleShortTime(DateTime? time);
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.App.Abstractions
{
public interface IPasswordRepromptService
{
string[] ProtectedFields { get; }
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
}
}

View File

@@ -11,7 +11,7 @@ namespace Bit.Core.Abstractions
/// <summary>
/// Gets the device type on the server enum
/// </summary>
DeviceType GetDevice();
Enums.DeviceType GetDevice();
string GetDeviceString();
ClientType GetClientType();
bool IsSelfHost();

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Bit.App.Models;
using Newtonsoft.Json.Linq;
namespace Bit.App.Abstractions
{
public interface IPushNotificationListenerService
{
Task OnMessageAsync(JObject values, string device);
Task OnRegisteredAsync(string token, string device);
void OnUnregistered(string device);
void OnError(string message, string device);
Task OnNotificationTapped(BaseNotificationData data);
Task OnNotificationDismissed(BaseNotificationData data);
bool ShouldShowNotification();
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
namespace Bit.App.Abstractions
{
public interface IPushNotificationService
{
bool IsRegisteredForPush { get; }
Task<bool> AreNotificationsSettingsEnabledAsync();
Task<string> GetTokenAsync();
Task RegisterAsync();
Task UnregisterAsync();
void SendLocalNotification(string title, string message, BaseNotificationData data);
void DismissLocalNotification(string notificationId);
}
}

8
src/Core/App.xaml Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.App">
<Application.Resources>
</Application.Resources>
</Application>

550
src/Core/App.xaml.cs Normal file
View File

@@ -0,0 +1,550 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Pages;
using Bit.Core.Resources.Localization;
using Bit.App.Services;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Bit.App
{
public partial class App : Application, IAccountsManagerHost
{
public const string POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE = "popAllAndGoToTabGenerator";
public const string POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE = "popAllAndGoToTabMyVault";
public const string POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE = "popAllAndGoToTabSend";
public const string POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE = "popAllAndGoToAutofillCiphers";
private readonly IBroadcasterService _broadcasterService;
private readonly IMessagingService _messagingService;
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ISyncService _syncService;
private readonly IAuthService _authService;
private readonly IDeviceActionService _deviceActionService;
private readonly IFileService _fileService;
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
private readonly IConfigService _configService;
private static bool _isResumed;
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
private static bool _pendingCheckPasswordlessLoginRequests;
private static object _processingLoginRequestLock = new object();
public App() : this(null)
{
}
public App(AppOptions appOptions)
{
Options = appOptions ?? new AppOptions();
if (Options.IosExtension)
{
Current = this;
return;
}
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_configService = ServiceContainer.Resolve<IConfigService>();
_accountsManager.Init(() => Options, this);
Bootstrap();
_broadcasterService.Subscribe(nameof(App), async (message) =>
{
try
{
if (message.Command == "showDialog")
{
var details = message.Data as DialogDetails;
var confirmed = true;
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
AppResources.Ok : details.ConfirmText;
Device.BeginInvokeOnMainThread(async () =>
{
if (!string.IsNullOrWhiteSpace(details.CancelText))
{
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
details.CancelText);
}
else
{
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
}
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
});
}
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
{
// 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.iOS)
{
ResumedAsync().FireAndForget();
}
}
else if (message.Command == "slept")
{
// 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.iOS)
{
await SleptAsync();
}
}
else if (message.Command == "migrated")
{
await Task.Delay(1000);
await _accountsManager.NavigateOnAccountChangeAsync();
}
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
{
if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
{
Options.OtpData = new OtpData((string)message.Data);
}
Device.InvokeOnMainThreadAsync(async () =>
{
if (Current.MainPage is TabsPage tabsPage)
{
while (tabsPage.Navigation.ModalStack.Count > 0)
{
await tabsPage.Navigation.PopModalAsync(false);
}
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
{
Current.MainPage = new NavigationPage(new CipherSelectionPage(Options));
}
else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE)
{
Options.MyVaultTile = false;
tabsPage.ResetToVaultPage();
}
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE)
{
Options.GeneratorTile = false;
tabsPage.ResetToGeneratorPage();
}
else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE)
{
tabsPage.ResetToSendPage();
}
else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
{
tabsPage.ResetToVaultPage();
await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
}
}
});
}
else if (message.Command == "convertAccountToKeyConnector")
{
Device.BeginInvokeOnMainThread(async () =>
{
await Application.Current.MainPage.Navigation.PushModalAsync(
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
else if (message.Command == Constants.ForceUpdatePassword)
{
Device.BeginInvokeOnMainThread(async () =>
{
await Application.Current.MainPage.Navigation.PushModalAsync(
new NavigationPage(new UpdateTempPasswordPage()));
});
}
else if (message.Command == "syncCompleted")
{
await _configService.GetAsync(true);
}
else if (message.Command == Constants.PasswordlessLoginRequestKey
|| message.Command == "unlocked"
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
lock (_processingLoginRequestLock)
{
// lock doesn't allow for async execution
CheckPasswordlessLoginRequestsAsync().Wait();
}
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
}
private async Task CheckPasswordlessLoginRequestsAsync()
{
if (!_isResumed)
{
_pendingCheckPasswordlessLoginRequests = true;
return;
}
_pendingCheckPasswordlessLoginRequests = false;
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
if (notification == null)
{
return;
}
if (await CheckShouldSwitchActiveUserAsync(notification))
{
return;
}
// Delay to wait for the vault page to appear
await Task.Delay(2000);
// if there is a request modal opened ignore all incoming requests
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
{
return;
}
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
{
PubKey = loginRequestData.PublicKey,
Id = loginRequestData.Id,
IpAddress = loginRequestData.RequestIpAddress,
Email = await _stateService.GetEmailAsync(),
FingerprintPhrase = loginRequestData.FingerprintPhrase,
RequestDate = loginRequestData.CreationDate,
DeviceType = loginRequestData.RequestDeviceType,
Origin = loginRequestData.Origin
});
await _stateService.SetPasswordlessLoginNotificationAsync(null);
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
if (!loginRequestData.IsExpired)
{
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
}
}
private async Task<bool> CheckShouldSwitchActiveUserAsync(PasswordlessRequestNotification notification)
{
var activeUserId = await _stateService.GetActiveUserIdAsync();
if (notification.UserId == activeUserId)
{
return false;
}
var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId);
Device.BeginInvokeOnMainThread(async () =>
{
try
{
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
if (result == AppResources.Ok)
{
await _stateService.SetActiveUserAsync(notification.UserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
return true;
}
public AppOptions Options { get; private set; }
protected async override void OnStart()
{
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
_isResumed = true;
await ClearCacheIfNeededAsync();
Prime();
if (string.IsNullOrWhiteSpace(Options.Uri))
{
var updated = await AppHelpers.PerformUpdateTasksAsync(_syncService, _deviceActionService,
_stateService);
if (!updated)
{
SyncIfNeeded();
}
}
if (_pendingCheckPasswordlessLoginRequests)
{
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
}
// 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)
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
// Reset delay on every start
_vaultTimeoutService.DelayLockAndLogoutMs = null;
}
await _configService.GetAsync();
_messagingService.Send("startEventTimer");
}
protected async override void OnSleep()
{
System.Diagnostics.Debug.WriteLine("XF App: OnSleep");
_isResumed = false;
// 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)
{
var isLocked = await _vaultTimeoutService.IsLockedAsync();
if (!isLocked)
{
await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime());
}
if (!SetTabsPageFromAutofill(isLocked))
{
ClearAutofillUri();
}
await SleptAsync();
}
}
protected override void OnResume()
{
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
_isResumed = true;
if (_pendingCheckPasswordlessLoginRequests)
{
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
}
// 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)
{
ResumedAsync().FireAndForget();
}
}
private async Task SleptAsync()
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
await ClearSensitiveFieldsAsync();
_messagingService.Send("stopEventTimer");
}
private async Task ResumedAsync()
{
await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync();
await _vaultTimeoutService.CheckVaultTimeoutAsync();
await ClearSensitiveFieldsAsync();
_messagingService.Send("startEventTimer");
await UpdateThemeAsync();
await ClearCacheIfNeededAsync();
Prime();
SyncIfNeeded();
if (Current.MainPage is NavigationPage navPage && navPage.CurrentPage is LockPage lockPage)
{
await lockPage.PromptBiometricAfterResumeAsync();
}
}
public async Task UpdateThemeAsync()
{
await Device.InvokeOnMainThreadAsync(() =>
{
ThemeManager.SetTheme(Current.Resources);
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
});
}
private async Task ClearSensitiveFieldsAsync()
{
await Device.InvokeOnMainThreadAsync(() =>
{
_messagingService.Send(Constants.ClearSensitiveFields);
});
}
private void SetCulture()
{
// Calendars are removed by linker. ref https://bugzilla.xamarin.com/show_bug.cgi?id=59077
new System.Globalization.ThaiBuddhistCalendar();
new System.Globalization.HijriCalendar();
new System.Globalization.UmAlQuraCalendar();
}
private async Task ClearCacheIfNeededAsync()
{
var lastClear = await _stateService.GetLastFileCacheClearAsync();
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
{
var task = Task.Run(() => _fileService.ClearCacheAsync());
}
}
private void ClearAutofillUri()
{
// 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 && !string.IsNullOrWhiteSpace(Options.Uri))
{
Options.Uri = null;
}
}
private bool SetTabsPageFromAutofill(bool isLocked)
{
// 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 && !string.IsNullOrWhiteSpace(Options.Uri) &&
!Options.FromAutofillFramework)
{
Task.Run(() =>
{
Device.BeginInvokeOnMainThread(() =>
{
Options.Uri = null;
if (isLocked)
{
Current.MainPage = new NavigationPage(new LockPage());
}
else
{
Current.MainPage = new TabsPage();
}
});
});
return true;
}
return false;
}
private void Prime()
{
Task.Run(() =>
{
var word = EEFLongWordList.Instance.List[1];
var parsedDomain = DomainName.TryParse("https://bitwarden.com", out var domainName);
});
}
private void Bootstrap()
{
InitializeComponent();
SetCulture();
ThemeManager.SetTheme(Current.Resources);
Current.RequestedThemeChanged += (s, a) =>
{
UpdateThemeAsync();
};
Current.MainPage = new NavigationPage(new HomePage(Options));
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
}
private void SyncIfNeeded()
{
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
{
return;
}
Task.Run(async () =>
{
var lastSync = await _syncService.GetLastSyncAsync();
if (lastSync == null || ((DateTime.UtcNow - lastSync) > TimeSpan.FromMinutes(30)))
{
await Task.Delay(1000);
await _syncService.FullSyncAsync(false);
}
});
}
public async Task SetPreviousPageInfoAsync()
{
PreviousPageInfo lastPageBeforeLock = null;
if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
{
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
if (topPage is NavigationPage navPage)
{
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
{
lastPageBeforeLock = new PreviousPageInfo
{
Page = "view",
CipherId = cipherDetailsPage.ViewModel.CipherId
};
}
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
{
lastPageBeforeLock = new PreviousPageInfo
{
Page = "edit",
CipherId = cipherAddEditPage.ViewModel.CipherId
};
}
}
}
await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock);
}
public void Navigate(NavigationTarget navTarget, INavigationParams navParams)
{
switch (navTarget)
{
case NavigationTarget.HomeLogin:
Current.MainPage = new NavigationPage(new HomePage(Options));
break;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)
{
Current.MainPage = new NavigationPage(new LoginPage(loginParams.Email, Options));
}
break;
case NavigationTarget.Lock:
if (navParams is LockNavigationParams lockParams)
{
Current.MainPage = new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric));
}
else
{
Current.MainPage = new NavigationPage(new LockPage(Options));
}
break;
case NavigationTarget.Home:
Current.MainPage = new TabsPage(Options);
break;
case NavigationTarget.AddEditCipher:
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
break;
case NavigationTarget.AutofillCiphers:
case NavigationTarget.OtpCipherSelection:
Current.MainPage = new NavigationPage(new CipherSelectionPage(Options));
break;
case NavigationTarget.SendAddEdit:
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
break;
}
}
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.Maui.Devices;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Behaviors
{
/// <summary>
/// This behavior prevents the Editor to be automatically scrolled to the bottom on focus.
/// This is needed due to this Xamarin Forms issue: https://github.com/xamarin/Xamarin.Forms/issues/2233
/// </summary>
public class EditorPreventAutoBottomScrollingOnFocusedBehavior : Behavior<Editor>
{
public static readonly BindableProperty ParentScrollViewProperty
= BindableProperty.Create(nameof(ParentScrollView), typeof(ScrollView), typeof(EditorPreventAutoBottomScrollingOnFocusedBehavior));
public ScrollView ParentScrollView
{
get => (ScrollView)GetValue(ParentScrollViewProperty);
set => SetValue(ParentScrollViewProperty, value);
}
protected override void OnAttachedTo(Editor bindable)
{
base.OnAttachedTo(bindable);
bindable.Focused += OnFocused;
}
private void OnFocused(object sender, FocusEventArgs e)
{
if (DeviceInfo.Platform.Equals(DevicePlatform.iOS) && ParentScrollView != null)
{
ParentScrollView.ScrollToAsync(ParentScrollView.ScrollX, ParentScrollView.ScrollY, true);
}
}
protected override void OnDetachingFrom(Editor bindable)
{
bindable.Focused -= OnFocused;
base.OnDetachingFrom(bindable);
}
}
}

View File

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

View File

@@ -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);
}
}
}
}

View File

@@ -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)));
}
}
}

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ViewCell 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"
x:Class="Bit.App.Controls.AccountViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core"
x:Name="_accountView"
x:DataType="controls:AccountViewCellViewModel">
<!--TODO: [MAUI-Migration] add long press
xct:TouchEffect.LongPressCommand="{Binding LongPressAccountCommand, Source={x:Reference _accountView}}"
xct:TouchEffect.LongPressCommandParameter="{Binding .}"-->
<Grid RowSpacing="0"
ColumnSpacing="0">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectAccountCommand, Source={x:Reference _accountView}}" CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Grid.Resources>
<u:InverseBoolConverter x:Key="inverseBool" />
</Grid.Resources>
<Grid
IsVisible="{Binding IsAccount}"
VerticalOptions="CenterAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Source="{Binding AvatarImageSource}"
HorizontalOptions="Center"
Margin="10,0"
VerticalOptions="Center" />
<Grid
Grid.Column="1"
RowSpacing="1"
VerticalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Grid.Row="0"
Text="{Binding AccountView.Email}"
IsVisible="{Binding IsActive}"
StyleClass="accountlist-title, accountlist-title-platform"
LineBreakMode="TailTruncation"
AutomationId="AccountEmailLabel" />
<Label
Grid.Row="0"
Text="{Binding AccountView.Email}"
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
StyleClass="accountlist-title, accountlist-title-platform"
TextColor="{DynamicResource MutedColor}"
LineBreakMode="TailTruncation"
AutomationId="AccountEmailLabel" />
<Label
Grid.Row="1"
IsVisible="{Binding ShowHostname}"
Text="{Binding AccountView.Hostname}"
StyleClass="accountlist-sub, accountlist-sub-platform"
LineBreakMode="TailTruncation"
AutomationId="AccountHostUrlLabel" />
<Label
Grid.Row="2"
Text="{u:I18n AccountUnlocked}"
IsVisible="{Binding IsUnlockedAndNotActive}"
StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic"
TextTransform="Lowercase"
LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
<Label
Grid.Row="2"
Text="{u:I18n AccountLocked}"
IsVisible="{Binding IsLockedAndNotActive}"
StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic"
TextTransform="Lowercase"
LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
<Label
Grid.Row="2"
Text="{u:I18n AccountLoggedOut}"
IsVisible="{Binding IsLoggedOutAndNotActive}"
StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic"
TextTransform="Lowercase"
LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
</Grid>
<controls:IconLabel
Grid.Column="2"
Text="{Binding AuthStatusIconNotActive}"
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
Margin="12,0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
AutomationId="InactiveVaultIcon" />
<controls:IconLabel
Grid.Column="2"
Text="{Binding AuthStatusIconActive}"
IsVisible="{Binding IsActive}"
Margin="12,0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
TextColor="{DynamicResource TextColor}"
AutomationId="ActiveVaultIcon" />
</Grid>
<Grid
IsVisible="{Binding IsAccount, Converter={StaticResource inverseBool}}"
VerticalOptions="CenterAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!--TODO: [MAUI-Migration] check that is the same-->
<!--<Image
Grid.Column="0"
VerticalOptions="Center"
HorizontalOptions="Center"
Margin="14,0"
WidthRequest="{OnPlatform 24, iOS=24, Android=26}"
HeightRequest="{OnPlatform 24, iOS=24, Android=26}"
Source="plus.png"
xct:IconTintColorEffect.TintColor="{DynamicResource TextColor}"
AutomationProperties.IsInAccessibleTree="False" />-->
<controls:IconLabel
Grid.Column="0"
VerticalOptions="Center"
HorizontalOptions="Center"
Margin="14,0"
TextColor="{DynamicResource TextColor}"
Text="{Binding Source={x:Static core:BitwardenIcons.Plus}}"
AutomationProperties.IsInAccessibleTree="False" />
<Label
Text="{u:I18n AddAccount}"
StyleClass="accountlist-title, accountlist-title-platform"
LineBreakMode="TailTruncation"
VerticalOptions="Center"
Grid.Column="1"
AutomationId="AddAccountButton" />
</Grid>
</Grid>
</ViewCell>

View File

@@ -0,0 +1,55 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class AccountViewCell : ViewCell
{
public static readonly BindableProperty AccountProperty = BindableProperty.Create(
nameof(Account), typeof(AccountView), typeof(AccountViewCell));
public static readonly BindableProperty SelectAccountCommandProperty = BindableProperty.Create(
nameof(SelectAccountCommand), typeof(ICommand), typeof(AccountViewCell));
public static readonly BindableProperty LongPressAccountCommandProperty = BindableProperty.Create(
nameof(LongPressAccountCommand), typeof(ICommand), typeof(AccountViewCell));
public AccountViewCell()
{
InitializeComponent();
}
public AccountView Account
{
get => GetValue(AccountProperty) as AccountView;
set => SetValue(AccountProperty, value);
}
public ICommand SelectAccountCommand
{
get => GetValue(SelectAccountCommandProperty) as ICommand;
set => SetValue(SelectAccountCommandProperty, value);
}
public ICommand LongPressAccountCommand
{
get => GetValue(LongPressAccountCommandProperty) as ICommand;
set => SetValue(LongPressAccountCommandProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == AccountProperty.PropertyName)
{
if (Account == null)
{
return;
}
BindingContext = new AccountViewCellViewModel(Account);
}
}
}
}

View File

@@ -0,0 +1,94 @@
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class AccountViewCellViewModel : ExtendedViewModel
{
private AccountView _accountView;
private AvatarImageSource _avatar;
public AccountViewCellViewModel(AccountView accountView)
{
AccountView = accountView;
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email, AccountView.AvatarColor);
}
public AccountView AccountView
{
get => _accountView;
set => SetProperty(ref _accountView, value);
}
public AvatarImageSource AvatarImageSource
{
get => _avatar;
set => SetProperty(ref _avatar, value);
}
public bool IsAccount
{
get => AccountView.IsAccount;
}
public bool ShowHostname
{
get => !string.IsNullOrWhiteSpace(AccountView.Hostname);
}
public bool IsActive
{
get => AccountView.IsActive;
}
public bool IsUnlocked
{
get => AccountView.AuthStatus == AuthenticationStatus.Unlocked;
}
public bool IsUnlockedAndNotActive
{
get => IsUnlocked && !IsActive;
}
public bool IsLocked
{
get => AccountView.AuthStatus == AuthenticationStatus.Locked;
}
public bool IsLockedAndNotActive
{
get => IsLocked && !IsActive;
}
public bool IsLoggedOut
{
get => AccountView.AuthStatus == AuthenticationStatus.LoggedOut;
}
public bool IsLoggedOutAndNotActive
{
get => IsLoggedOut && !IsActive;
}
public string AuthStatusIconActive
{
get => BitwardenIcons.CheckCircle;
}
public string AuthStatusIconNotActive
{
get
{
if (IsUnlocked)
{
return BitwardenIcons.Unlock;
}
return BitwardenIcons.Lock;
}
}
}
}

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<controls:ExtendedGrid xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.AuthenticatorViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:ff="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Compat.Maui"
xmlns:core="clr-namespace:Bit.Core"
StyleClass="list-row, list-row-platform"
HorizontalOptions="FillAndExpand"
x:DataType="pages:GroupingsPageTOTPListItem"
ColumnDefinitions="40,*,40,Auto,40"
RowSpacing="0"
Padding="0,10,0,0"
RowDefinitions="*,*">
<Grid.Resources>
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:InverseBoolConverter x:Key="inverseBool" />
</Grid.Resources>
<controls:IconLabel
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
AutomationProperties.IsInAccessibleTree="False" />
<ff:CachedImage
Grid.Column="0"
BitmapOptimizations="True"
ErrorPlaceholder="login.png"
LoadingPlaceholder="login.png"
HorizontalOptions="Center"
VerticalOptions="Center"
WidthRequest="22"
HeightRequest="22"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage}"
Source="{Binding IconImageSource, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="False" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="0"
VerticalTextAlignment="Center"
VerticalOptions="Fill"
StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="1"
VerticalTextAlignment="Center"
VerticalOptions="Fill"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}" />
<controls:CircularProgressbarView
Progress="{Binding Progress}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
StyleClass="text-sm"
HorizontalTextAlignment="Center"
HorizontalOptions="Fill"
VerticalTextAlignment="Center"
VerticalOptions="Fill" />
<StackLayout
Grid.Row="0"
Grid.Column="3"
Margin="3,0,2,0"
Spacing="5"
Grid.RowSpan="2"
Orientation="Horizontal"
HorizontalOptions="Fill"
VerticalOptions="Fill">
<controls:MonoLabel
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
Style="{DynamicResource textTotp}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand" />
<controls:MonoLabel
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
Style="{DynamicResource textTotp}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="4"
Grid.RowSpan="2"
Padding="0,0,1,0"
HorizontalOptions="Center"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n CopyTotp}" />
</controls:ExtendedGrid>

View File

@@ -0,0 +1,68 @@
using System;
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 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()
{
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

@@ -0,0 +1,182 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Utilities;
using SkiaSharp;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class AvatarImageSource : StreamImageSource
{
private readonly string _text;
private readonly string _id;
private readonly string _color;
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (obj is AvatarImageSource avatar)
{
return avatar._id == _id && avatar._text == _text && avatar._color == _color;
}
return base.Equals(obj);
}
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null)
{
_id = userId;
_text = name;
if (string.IsNullOrWhiteSpace(_text))
{
_text = email;
}
_color = color;
}
public override Func<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
private Task<Stream> GetStreamAsync(CancellationToken userToken = new CancellationToken())
{
// TODO: [MAUI-Migration] [Critical] now methods are private protected so cannot be used here, figure out workaround
//OnLoadingStarted();
//userToken.Register(CancellationTokenSource.Cancel);
var result = Draw();
//OnLoadingCompleted(CancellationTokenSource.IsCancellationRequested);
return Task.FromResult(result);
}
private Stream Draw()
{
string chars;
string upperCaseText = null;
if (string.IsNullOrEmpty(_text))
{
chars = "..";
}
else if (_text?.Length > 1)
{
upperCaseText = _text.ToUpper();
chars = GetFirstLetters(upperCaseText, 2);
}
else
{
chars = upperCaseText = _text.ToUpper();
}
var bgColor = _color ?? CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
var size = 50;
using (var bitmap = new SKBitmap(size * 2,
size * 2,
SKImageInfo.PlatformColorType,
SKAlphaType.Premul))
{
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent);
using (var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor)
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
using (var circlePaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor)
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
using (var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
})
{
var rect = new SKRect();
textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
using (var img = SKImage.FromBitmap(bitmap))
{
var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data?.AsStream(true);
}
}
}
}
}
}
}
private string GetFirstLetters(string data, int charCount)
{
var sanitizedData = data.Trim();
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1 && charCount <= 2)
{
var text = string.Empty;
for (var i = 0; i < charCount; i++)
{
text += parts[i][0];
}
return text;
}
if (sanitizedData.Length > 2)
{
return sanitizedData.Substring(0, 2);
}
return sanitizedData;
}
private Color StringToColor(string str)
{
if (str == null)
{
return Color.FromArgb("#33ffffff");
}
var hash = 0;
for (var i = 0; i < str.Length; i++)
{
hash = str[i] + ((hash << 5) - hash);
}
var color = "#FF";
for (var i = 0; i < 3; i++)
{
var value = (hash >> (i * 8)) & 0xff;
var base16 = "00" + Convert.ToString(value, 16);
color += base16.Substring(base16.Length - 2);
}
return Color.FromArgb(color);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Concurrent;
namespace Bit.App.Controls
{
public interface IAvatarImageSourcePool
{
AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color);
}
public class AvatarImageSourcePool : IAvatarImageSourcePool
{
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color)
{
var key = $"{userId}{name}{email}{color}";
if (!_cache.TryGetValue(key, out var avatar))
{
avatar = new AvatarImageSource(userId, name, email, color);
if (!_cache.TryAdd(key, avatar)
&&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
{
// if add and get after fails, then something wrong is going on with this method.
throw new InvalidOperationException("Something is wrong creating the avatar image");
}
}
return avatar;
}
}
}

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<controls:ExtendedGrid xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.CipherViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:ff="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Compat.Maui"
xmlns:core="clr-namespace:Bit.Core"
StyleClass="list-row, list-row-platform"
RowSpacing="0"
ColumnSpacing="0"
x:DataType="controls:CipherViewCellViewModel"
AutomationId="CipherCell">
<Grid.Resources>
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
<u:IconImageConverter x:Key="iconImageConverter"/>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:StringHasValueConverter x:Key="stringHasValueConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" x:Name="_iconColumn" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<controls:IconLabel
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
AutomationProperties.IsInAccessibleTree="False"
AutomationId="CipherTypeIcon" />
<ff:CachedImage
x:Name="_iconImage"
Grid.Column="0"
BitmapOptimizations="True"
ErrorPlaceholder="login.png"
LoadingPlaceholder="login.png"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"
Margin="9"
WidthRequest="22"
HeightRequest="22"
Aspect="AspectFit"
IsVisible="{Binding ShowIconImage}"
Source="{Binding IconImageSource, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="False"
AutomationId="CipherWebsiteIcon" />
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="0"
StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}"
AutomationId="CipherNameLabel" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="3"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}"
IsVisible="{Binding Source={RelativeSource Self}, Path=Text,
Converter={StaticResource stringHasValueConverter}}"
AutomationId="CipherSubTitleLabel" />
<controls:IconLabel
Grid.Column="1"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Shared}"
AutomationId="CipherInCollectionIcon" />
<controls:IconLabel
Grid.Column="2"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}"
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Attachments}"
AutomationId="CipherWithAttachmentsIcon" />
</Grid>
<controls:MiButton
Grid.Row="0"
Grid.Column="2"
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
Clicked="MoreButton_Clicked"
VerticalOptions="CenterAndExpand"
HorizontalOptions="EndAndExpand"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Options}"
AutomationId="CipherOptionsButton" />
</controls:ExtendedGrid>

View File

@@ -0,0 +1,83 @@
using System;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class CipherViewCell : ExtendedGrid
{
private const int ICON_COLUMN_DEFAULT_WIDTH = 40;
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(
nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell));
public CipherViewCell()
{
InitializeComponent();
var fontScale = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService").GetSystemFontSizeScale();
_iconColumn.Width = new GridLength(ICON_COLUMN_DEFAULT_WIDTH * fontScale, GridUnitType.Absolute);
_iconImage.WidthRequest = 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
{
get => GetValue(ButtonCommandProperty) as ICommand;
set => SetValue(ButtonCommandProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == CipherProperty.PropertyName)
{
if (Cipher == null)
{
return;
}
BindingContext = new CipherViewCellViewModel(Cipher, WebsiteIconsEnabled ?? false);
}
else if (propertyName == WebsiteIconsEnabledProperty.PropertyName)
{
if (Cipher == null)
{
return;
}
((CipherViewCellViewModel)BindingContext).WebsiteIconsEnabled = WebsiteIconsEnabled ?? false;
}
}
private void MoreButton_Clicked(object sender, EventArgs e)
{
var cipher = ((sender as MiButton)?.BindingContext as CipherViewCellViewModel)?.Cipher;
if (cipher != null)
{
ButtonCommand?.Execute(cipher);
}
}
}
}

View File

@@ -0,0 +1,51 @@
using Bit.App.Utilities;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
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

@@ -0,0 +1,142 @@
using System;
using System.Runtime.CompilerServices;
using SkiaSharp;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using SkiaSharp.Views.Maui.Controls;
using SkiaSharp.Views.Maui;
namespace Bit.App.Controls
{
public class CircularProgressbarView : SKCanvasView
{
private Circle _circle;
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromArgb("175DDC"));
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromArgb("dd4b39"));
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Colors.White);
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public float Radius
{
get => (float)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
public float StrokeWidth
{
get => (float)GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}
public Color ProgressColor
{
get => (Color)GetValue(ProgressColorProperty);
set => SetValue(ProgressColorProperty, value);
}
public Color EndingProgressColor
{
get => (Color)GetValue(EndingProgressColorProperty);
set => SetValue(EndingProgressColorProperty, value);
}
public Color BackgroundProgressColor
{
get => (Color)GetValue(BackgroundProgressColorProperty);
set => SetValue(BackgroundProgressColorProperty, value);
}
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var context = bindable as CircularProgressbarView;
context.InvalidateSurface();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(Progress))
{
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
}
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
if (_circle != null)
{
_circle.CalculateCenter(e.Info);
e.Surface.Canvas.Clear();
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
}
}
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
{
canvas.DrawCircle(circle.Center, circle.Redius,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = color,
IsStroke = true,
IsAntialias = true
});
}
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
{
var progressValue = progress();
var angle = progressValue * 3.6f;
canvas.DrawArc(circle.Rect, 270, angle, false,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = progressValue < 20f ? progressEndColor : color,
IsStroke = true,
IsAntialias = true
});
}
}
public class Circle
{
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
{
_centerFunc = centerFunc;
Redius = redius;
}
public SKPoint Center { get; set; }
public float Redius { get; set; }
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
public void CalculateCenter(SKImageInfo argsInfo)
{
Center = _centerFunc(argsInfo);
}
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class CustomLabel : Label
{
public CustomLabel()
{
}
public int? FontWeight { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Grid
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.DateTimePicker"
ColumnDefinitions="*,*">
<controls:ExtendedDatePicker
x:Name="_datePicker"
Grid.Column="0"
NullableDate="{Binding Date, Mode=TwoWay}"
Format="d"
AutomationProperties.IsInAccessibleTree="True" />
<controls:ExtendedTimePicker
x:Name="_timePicker"
Grid.Column="1"
NullableTime="{Binding Time, Mode=TwoWay}"
Format="t"
AutomationProperties.IsInAccessibleTree="True" />
</Grid>

View File

@@ -0,0 +1,40 @@
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using CommunityToolkit.Maui.Converters;
using CommunityToolkit.Maui.ImageSources;
using CommunityToolkit.Maui;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Layouts;
using CommunityToolkit.Maui.Views;
namespace Bit.App.Controls
{
public partial class DateTimePicker : Grid
{
public DateTimePicker()
{
InitializeComponent();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
BindingContext is DateTimeViewModel dateTimeViewModel)
{
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
}
}
}
public class LazyDateTimePicker : LazyView<DateTimePicker>
{
}
}

View File

@@ -0,0 +1,70 @@
using System;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class DateTimeViewModel : ExtendedViewModel
{
DateTime? _date;
TimeSpan? _time;
public DateTimeViewModel(string dateName, string timeName)
{
DateName = dateName;
TimeName = timeName;
}
public Action<DateTime?> OnDateChanged { get; set; }
public Action<TimeSpan?> OnTimeChanged { get; set; }
public DateTime? Date
{
get => _date;
set
{
if (SetProperty(ref _date, value))
{
OnDateChanged?.Invoke(value);
}
}
}
public TimeSpan? Time
{
get => _time;
set
{
if (SetProperty(ref _time, value))
{
OnTimeChanged?.Invoke(value);
}
}
}
public string DateName { get; }
public string TimeName { get; }
public string DatePlaceholder { get; set; }
public string TimePlaceholder { get; set; }
public DateTime? DateTime
{
get
{
if (Date.HasValue)
{
if (Time.HasValue)
{
return Date.Value.Add(Time.Value);
}
return Date;
}
return null;
}
set
{
Date = value?.Date;
Time = value?.Date.TimeOfDay;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Globalization;
using CommunityToolkit.Maui.Converters;
namespace Bit.App.Controls
{
public class ExtendedCollectionView : CollectionView
{
public string ExtraDataForLogging { get; set; }
}
public class SelectionChangedEventArgsConverter : BaseConverterOneWay<SelectionChangedEventArgs, object>
{
public override object DefaultConvertReturnValue { get; set; } = null;
public override object ConvertFrom(SelectionChangedEventArgs value, CultureInfo culture)
{
return value?.CurrentSelection.FirstOrDefault();
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedDatePicker : DatePicker
{
private string _format;
public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create(
nameof(PlaceHolder), typeof(string), typeof(ExtendedDatePicker));
public string PlaceHolder
{
get { return (string)GetValue(PlaceHolderProperty); }
set { SetValue(PlaceHolderProperty, value); }
}
public static readonly BindableProperty NullableDateProperty = BindableProperty.Create(
nameof(NullableDate), typeof(DateTime?), typeof(ExtendedDatePicker));
public DateTime? NullableDate
{
get { return (DateTime?)GetValue(NullableDateProperty); }
set
{
SetValue(NullableDateProperty, value);
UpdateDate();
}
}
private void UpdateDate()
{
if (NullableDate.HasValue)
{
if (_format != null)
{
Format = _format;
}
}
else
{
Format = PlaceHolder;
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
_format = Format;
UpdateDate();
}
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == DateProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName &&
!IsFocused && (Date.ToString("d") ==
DateTime.Now.ToString("d"))))
{
NullableDate = Date;
UpdateDate();
}
if (propertyName == NullableDateProperty.PropertyName)
{
if (NullableDate.HasValue)
{
Date = NullableDate.Value;
if (Date.ToString(_format) == DateTime.Now.ToString(_format))
{
UpdateDate();
}
}
else
{
UpdateDate();
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedGrid : Grid
{
}
}

View File

@@ -0,0 +1,22 @@
using Bit.App.Utilities;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedSearchBar : SearchBar
{
public ExtendedSearchBar()
{
// 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.iOS)
{
if (ThemeManager.UsingLightTheme)
{
TextColor = Colors.Black;
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedSlider : Slider
{
public static readonly BindableProperty ThumbBorderColorProperty = BindableProperty.Create(
nameof(ThumbBorderColor), typeof(Color), typeof(ExtendedSlider), Color.FromArgb("b5b5b5"));
public Color ThumbBorderColor
{
get => (Color)GetValue(ThumbBorderColorProperty);
set => SetValue(ThumbBorderColorProperty, value);
}
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedStackLayout : StackLayout
{
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedStepper : Stepper
{
public static readonly BindableProperty StepperBackgroundColorProperty = BindableProperty.Create(
nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Colors.White);
public static readonly BindableProperty StepperForegroundColorProperty = BindableProperty.Create(
nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Colors.Black);
public Color StepperBackgroundColor
{
get => (Color)GetValue(StepperBackgroundColorProperty);
set => SetValue(StepperBackgroundColorProperty, value);
}
public Color StepperForegroundColor
{
get => (Color)GetValue(StepperForegroundColorProperty);
set => SetValue(StepperForegroundColorProperty, value);
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedTimePicker : TimePicker
{
private string _format;
public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create(
nameof(PlaceHolder), typeof(string), typeof(ExtendedTimePicker));
public string PlaceHolder
{
get { return (string)GetValue(PlaceHolderProperty); }
set { SetValue(PlaceHolderProperty, value); }
}
public static readonly BindableProperty NullableTimeProperty = BindableProperty.Create(
nameof(NullableTime), typeof(TimeSpan?), typeof(ExtendedTimePicker));
public TimeSpan? NullableTime
{
get { return (TimeSpan?)GetValue(NullableTimeProperty); }
set
{
SetValue(NullableTimeProperty, value);
UpdateTime();
}
}
private void UpdateTime()
{
if (NullableTime.HasValue)
{
if (_format != null)
{
Format = _format;
}
}
else
{
Format = PlaceHolder;
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
_format = Format;
UpdateTime();
}
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TimeProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName &&
!IsFocused && (Time.ToString("t") ==
DateTime.Now.TimeOfDay.ToString("t"))))
{
NullableTime = Time;
UpdateTime();
}
if (propertyName == NullableTimeProperty.PropertyName)
{
if (NullableTime.HasValue)
{
Time = NullableTime.Value;
if (Time.ToString(_format) == DateTime.Now.TimeOfDay.ToString(_format))
{
UpdateTime();
}
}
else
{
UpdateTime();
}
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class ExtendedToolbarItem : ToolbarItem
{
public bool UseOriginalImage { get; set; }
// HACK: For the issue of correctly updating the avatar toolbar item color on iOS
// we need to subscribe to the PropertyChanged event of the ToolbarItem on the CustomNavigationRenderer
// The problem is that there are a lot of private places where the navigation renderer disposes objects
// that we don't have access to, and that we should in order to properly prevent memory leaks
// So as a hack solution we have this OnAppearing/OnDisappearing actions and methods to be called on page lifecycle
// to subscribe/unsubscribe indirectly on the CustomNavigationRenderer
public Action OnAppearingAction { get; set; }
public Action OnDisappearingAction { get; set; }
public void OnAppearing()
{
OnAppearingAction?.Invoke();
}
public void OnDisappearing()
{
OnDisappearingAction?.Invoke();
}
}
}

View File

@@ -0,0 +1,29 @@
<?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:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.ExternalLinkItemView"
xmlns:core="clr-namespace:Bit.Core"
x:Name="_contentView">
<ContentView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToLinkCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
</ContentView.GestureRecognizers>
<StackLayout
Orientation="Horizontal">
<controls:CustomLabel
Text="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}"
HorizontalOptions="StartAndExpand"
LineBreakMode="TailTruncation" />
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
TextColor="{DynamicResource TextColor}"
HorizontalOptions="End"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
</StackLayout>
</ContentView>

View File

@@ -0,0 +1,32 @@
using System.Windows.Input;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class ExternalLinkItemView : ContentView
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(ExternalLinkItemView), null, BindingMode.OneWay);
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkItemView));
public ExternalLinkItemView()
{
InitializeComponent();
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public ICommand GoToLinkCommand
{
get => GetValue(GoToLinkCommandProperty) as ICommand;
set => SetValue(GoToLinkCommandProperty, value);
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class HybridWebView : View
{
private Action<string> _func;
public static readonly BindableProperty UriProperty = BindableProperty.Create(propertyName: nameof(Uri),
returnType: typeof(string), declaringType: typeof(HybridWebView), defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
public void RegisterAction(Action<string> callback)
{
_func = callback;
}
public void Cleanup()
{
_func = null;
}
public void InvokeAction(string data)
{
_func?.Invoke(data);
}
}
}

View File

@@ -0,0 +1,26 @@
using Bit.App.Effects;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class IconButton : Button
{
public IconButton()
{
Padding = 0;
// 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
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "bwi-font";
break;
case Device.Android:
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View File

@@ -0,0 +1,27 @@
using Bit.App.Effects;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class IconLabel : Label
{
public bool ShouldUpdateFontSizeDynamicallyForAccesibility { get; set; }
public IconLabel()
{
// 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
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "bwi-font";
break;
case Device.Android:
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<Frame xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.IconLabelButton"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Name="_iconLabelButton"
HeightRequest="45"
Padding="1"
StyleClass="btn-icon-secondary"
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
BorderColor="Transparent"
HasShadow="False">
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
</Frame.GestureRecognizers>
<Frame
Margin="0"
Padding="0"
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
BorderColor="Transparent"
IsClippedToBounds="True"
HasShadow="False">
<StackLayout
Orientation="Horizontal"
HorizontalOptions="Center">
<controls:IconLabel
VerticalOptions="Center"
HorizontalTextAlignment="Center"
FontSize="Large"
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
Text="{Binding Icon, Source={x:Reference _iconLabelButton}}">
</controls:IconLabel>
<Label
VerticalOptions="Center"
HorizontalTextAlignment="Center"
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
FontSize="Medium"
Text="{Binding Label, Source={x:Reference _iconLabelButton}}"/>
</StackLayout>
</Frame>
</Frame>

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.Core.Models.Domain;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class IconLabelButton : Frame
{
public static readonly BindableProperty IconProperty = BindableProperty.Create(
nameof(Icon), typeof(string), typeof(IconLabelButton));
public static readonly BindableProperty LabelProperty = BindableProperty.Create(
nameof(Label), typeof(string), typeof(IconLabelButton));
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(ICommand), typeof(IconLabelButton));
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Colors.White);
public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create(
nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Colors.White);
public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create(
nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Colors.White);
public IconLabelButton()
{
InitializeComponent();
}
public string Icon
{
get => GetValue(IconProperty) as string;
set => SetValue(IconProperty, value);
}
public string Label
{
get => GetValue(LabelProperty) as string;
set => SetValue(LabelProperty, value);
}
public ICommand ButtonCommand
{
get => GetValue(ButtonCommandProperty) as ICommand;
set => SetValue(ButtonCommandProperty, value);
}
public Color IconLabelColor
{
get { return (Color)GetValue(IconLabelColorProperty); }
set { SetValue(IconLabelColorProperty, value); }
}
public Color IconLabelBackgroundColor
{
get { return (Color)GetValue(IconLabelBackgroundColorProperty); }
set { SetValue(IconLabelBackgroundColorProperty, value); }
}
public Color IconLabelBorderColor
{
get { return (Color)GetValue(IconLabelBorderColorProperty); }
set { SetValue(IconLabelBorderColorProperty, value); }
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class MiButton : Button
{
public MiButton()
{
Padding = 0;
// 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
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "Material Icons";
break;
case Device.Android:
FontFamily = "MaterialIcons_Regular.ttf#Material Icons";
break;
}
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class MiLabel : Label
{
public MiLabel()
{
// 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
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "Material Icons";
break;
case Device.Android:
FontFamily = "MaterialIcons_Regular.ttf#Material Icons";
break;
}
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class MonoEntry : Entry
{
public MonoEntry()
{
// 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
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "Menlo-Regular";
break;
case Device.Android:
FontFamily = "RobotoMono_Regular.ttf#Roboto Mono";
break;
}
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class MonoLabel : Label
{
public MonoLabel()
{
// 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
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "Menlo-Regular";
break;
case Device.Android:
FontFamily = "RobotoMono_Regular.ttf#Roboto Mono";
break;
}
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Bit.App.Controls
{
public interface IPasswordStrengthable
{
string Password { get; }
List<string> UserInputs { get; }
}
}

View File

@@ -0,0 +1,17 @@
using Bit.Core.Attributes;
namespace Bit.App.Controls
{
public enum PasswordStrengthLevel
{
[LocalizableEnum("Weak")]
VeryWeak,
[LocalizableEnum("Weak")]
Weak,
[LocalizableEnum("Good")]
Good,
[LocalizableEnum("Strong")]
Strong
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<StackLayout
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="controls:PasswordStrengthViewModel"
x:Class="Bit.App.Controls.PasswordStrengthProgressBar"
StyleClass="box">
<StackLayout.Resources>
<ResourceDictionary>
<u:LocalizableEnumConverter x:Key="localizableEnum" />
</ResourceDictionary>
</StackLayout.Resources>
<ProgressBar
x:Name="_progressBar"
u:ProgressBarExtensions.AnimatedProgress="{Binding PasswordStrength}"
ScaleY="2" />
<Label
x:Name="_progressLabel"
Text="{Binding PasswordStrengthLevel, Converter={StaticResource localizableEnum}, TargetNullValue=' ' }"
StyleClass="box-footer-label" />
</StackLayout>

View File

@@ -0,0 +1,110 @@
using System;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class PasswordStrengthProgressBar : StackLayout
{
public static readonly BindableProperty PasswordStrengthLevelProperty = BindableProperty.Create(
nameof(PasswordStrengthLevel),
typeof(PasswordStrengthLevel),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty VeryWeakColorProperty = BindableProperty.Create(
nameof(VeryWeakColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty WeakColorProperty = BindableProperty.Create(
nameof(WeakColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty GoodColorProperty = BindableProperty.Create(
nameof(GoodColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public static readonly BindableProperty StrongColorProperty = BindableProperty.Create(
nameof(StrongColor),
typeof(Color),
typeof(PasswordStrengthProgressBar),
propertyChanged: OnControlPropertyChanged);
public PasswordStrengthLevel? PasswordStrengthLevel
{
get { return (PasswordStrengthLevel?)GetValue(PasswordStrengthLevelProperty); }
set { SetValue(PasswordStrengthLevelProperty, value); }
}
public Color VeryWeakColor
{
get { return (Color)GetValue(VeryWeakColorProperty); }
set { SetValue(VeryWeakColorProperty, value); }
}
public Color WeakColor
{
get { return (Color)GetValue(WeakColorProperty); }
set { SetValue(WeakColorProperty, value); }
}
public Color GoodColor
{
get { return (Color)GetValue(GoodColorProperty); }
set { SetValue(GoodColorProperty, value); }
}
public Color StrongColor
{
get { return (Color)GetValue(StrongColorProperty); }
set { SetValue(StrongColorProperty, value); }
}
public PasswordStrengthProgressBar()
{
InitializeComponent();
SetBinding(PasswordStrengthProgressBar.PasswordStrengthLevelProperty, new Binding() { Path = nameof(PasswordStrengthViewModel.PasswordStrengthLevel) });
UpdateColors();
}
private static void OnControlPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as PasswordStrengthProgressBar)?.UpdateColors();
}
public void UpdateColors()
{
if (_progressBar == null || _progressLabel == null)
{
return;
}
_progressBar.ProgressColor = GetColorForStrength();
_progressLabel.TextColor = _progressBar.ProgressColor;
}
private Color GetColorForStrength()
{
switch (PasswordStrengthLevel)
{
case Controls.PasswordStrengthLevel.VeryWeak:
return VeryWeakColor;
case Controls.PasswordStrengthLevel.Weak:
return WeakColor;
case Controls.PasswordStrengthLevel.Good:
return GoodColor;
case Controls.PasswordStrengthLevel.Strong:
return StrongColor;
default:
return Colors.Transparent;
}
}
}
}

View File

@@ -0,0 +1,68 @@
using System.Collections.Generic;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class PasswordStrengthViewModel : ExtendedViewModel
{
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPasswordStrengthable _passwordStrengthable;
private double _passwordStrength;
private Color _passwordColor;
private PasswordStrengthLevel? _passwordStrengthLevel;
public PasswordStrengthViewModel(IPasswordStrengthable passwordStrengthable)
{
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_passwordStrengthable = passwordStrengthable;
}
public double PasswordStrength
{
get => _passwordStrength;
set => SetProperty(ref _passwordStrength, value);
}
public PasswordStrengthLevel? PasswordStrengthLevel
{
get => _passwordStrengthLevel;
set => SetProperty(ref _passwordStrengthLevel, value);
}
public List<string> GetPasswordStrengthUserInput(string email) => _passwordGenerationService.GetPasswordStrengthUserInput(email);
public void CalculatePasswordStrength()
{
if (string.IsNullOrEmpty(_passwordStrengthable.Password))
{
PasswordStrength = 0;
PasswordStrengthLevel = null;
return;
}
var passwordStrength = _passwordGenerationService.PasswordStrength(_passwordStrengthable.Password, _passwordStrengthable.UserInputs);
// The passwordStrength.Score is 0..4, convertion was made to be used as a progress directly by the control 0..1
PasswordStrength = (passwordStrength.Score + 1f) / 5f;
if (PasswordStrength <= 0.4f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.VeryWeak;
}
else if (PasswordStrength <= 0.6f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Weak;
}
else if (PasswordStrength <= 0.8f)
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Good;
}
else
{
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Strong;
}
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
[Obsolete]
public class RepeaterView : StackLayout
{
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
nameof(ItemTemplate), typeof(DataTemplate), typeof(RepeaterView), default(DataTemplate));
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
nameof(ItemsSource), typeof(ICollection), typeof(RepeaterView), null, BindingMode.OneWay,
propertyChanged: ItemsSourceChanging);
public RepeaterView()
{
Spacing = 0;
}
public ICollection ItemsSource
{
get => GetValue(ItemsSourceProperty) as ICollection;
set => SetValue(ItemsSourceProperty, value);
}
public DataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty) as DataTemplate;
set => SetValue(ItemTemplateProperty, value);
}
private void OnCollectionChanged(object sender,
NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
Populate();
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == ItemTemplateProperty.PropertyName || propertyName == ItemsSourceProperty.PropertyName)
{
Populate();
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
Populate();
}
protected virtual View ViewFor(object item)
{
View view = null;
var template = ItemTemplate;
if (template != null)
{
if (template is DataTemplateSelector selector)
{
template = selector.SelectTemplate(item, this);
}
var content = template.CreateContent();
view = content is View ? content as View : ((ViewCell)content).View;
view.BindingContext = item;
}
return view;
}
private void Populate()
{
if (ItemsSource != null)
{
Children.Clear();
foreach (var item in ItemsSource)
{
Children.Add(ViewFor(item));
}
}
}
private static void ItemsSourceChanging(BindableObject bindable, object oldValue, object newValue)
{
if (oldValue != null && oldValue is INotifyCollectionChanged ov)
{
ov.CollectionChanged -= (bindable as RepeaterView).OnCollectionChanged;
}
if (newValue != null && newValue is INotifyCollectionChanged nv)
{
nv.CollectionChanged += (bindable as RepeaterView).OnCollectionChanged;
}
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class SelectableLabel : Label
{
}
}

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<controls:ExtendedGrid xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.SendViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core"
StyleClass="list-row, list-row-platform"
RowSpacing="0"
ColumnSpacing="0"
x:DataType="controls:SendViewCellViewModel">
<Grid.Resources>
<u:SendIconGlyphConverter x:Key="sendIconGlyphConverter"/>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" x:Name="_iconColumn" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<controls:IconLabel
Grid.Row="0"
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
Text="{Binding Send, Converter={StaticResource sendIconGlyphConverter}}"
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
AutomationProperties.IsInAccessibleTree="False" />
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="0"
StyleClass="list-title, list-title-platform"
Text="{Binding Send.Name}"
AutomationId="SendNameLabel" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="6"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Send.DisplayDate}"
AutomationId="SendDateLabel" />
<controls:IconLabel
Grid.Column="1"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Disabled}"
AutomationId="DisabledSendLabel" />
<controls:IconLabel
Grid.Column="2"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Password}"
AutomationId="PasswordProtectedSendLabel" />
<controls:IconLabel
Grid.Column="3"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n MaxAccessCountReached}"
AutomationId="SendMaxAccessCountReachedLabel" />
<controls:IconLabel
Grid.Column="4"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
IsVisible="{Binding Send.Expired, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Expired}"
AutomationId="ExpiredSendLabel" />
<controls:IconLabel
Grid.Column="5"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n PendingDelete}"
AutomationId="SendWithPendingDeletionLabel" />
</Grid>
<controls:MiButton
Grid.Row="0"
Grid.Column="2"
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
IsVisible="{Binding ShowOptions, Mode=OneWay}"
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
Clicked="MoreButton_Clicked"
VerticalOptions="CenterAndExpand"
HorizontalOptions="EndAndExpand"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Options}"
AutomationId="SendOptionsButton" />
</controls:ExtendedGrid>

View File

@@ -0,0 +1,69 @@
using System;
using Bit.App.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class SendViewCell : ExtendedGrid
{
public static readonly BindableProperty SendProperty = BindableProperty.Create(
nameof(Send), typeof(SendView), typeof(SendViewCell), default(SendView), BindingMode.OneWay);
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(Command<SendView>), typeof(SendViewCell));
public static readonly BindableProperty ShowOptionsProperty = BindableProperty.Create(
nameof(ShowOptions), typeof(bool), typeof(SendViewCell), true, BindingMode.OneWay);
public SendViewCell()
{
InitializeComponent();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_iconColumn.Width = new GridLength(40 * deviceActionService.GetSystemFontSizeScale(), GridUnitType.Absolute);
}
public SendView Send
{
get => GetValue(SendProperty) as SendView;
set => SetValue(SendProperty, value);
}
public Command<SendView> ButtonCommand
{
get => GetValue(ButtonCommandProperty) as Command<SendView>;
set => SetValue(ButtonCommandProperty, value);
}
public bool ShowOptions
{
get => (bool)GetValue(ShowOptionsProperty);
set => SetValue(ShowOptionsProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == SendProperty.PropertyName)
{
if (Send == null)
{
return;
}
BindingContext = new SendViewCellViewModel(Send, ShowOptions);
}
}
private void MoreButton_Clicked(object sender, EventArgs e)
{
var send = ((sender as MiButton)?.BindingContext as SendViewCellViewModel)?.Send;
if (send != null)
{
ButtonCommand?.Execute(send);
}
}
}
}

View File

@@ -0,0 +1,29 @@
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class SendViewCellViewModel : ExtendedViewModel
{
private SendView _send;
private bool _showOptions;
public SendViewCellViewModel(SendView sendView, bool showOptions)
{
Send = sendView;
ShowOptions = showOptions;
}
public SendView Send
{
get => _send;
set => SetProperty(ref _send, value);
}
public bool ShowOptions
{
get => _showOptions;
set => SetProperty(ref _showOptions, value);
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public class BaseSettingItemView : ContentView
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
public static readonly BindableProperty SubtitleProperty = BindableProperty.Create(
nameof(Subtitle), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public string Subtitle
{
get { return (string)GetValue(SubtitleProperty); }
set { SetValue(SubtitleProperty, value); }
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<controls:BaseSettingItemView
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.SettingChooserItemView"
x:Name="_contentView"
ControlTemplate="{StaticResource SettingControlTemplate}">
<controls:BaseSettingItemView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ChooseCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
</controls:BaseSettingItemView.GestureRecognizers>
<controls:CustomLabel
Text="{Binding DisplayValue, Source={x:Reference _contentView}}"
HorizontalTextAlignment="End"
TextColor="{DynamicResource MutedColor}"
StyleClass="list-sub" />
</controls:BaseSettingItemView>

View File

@@ -0,0 +1,32 @@
using System.Windows.Input;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class SettingChooserItemView : BaseSettingItemView
{
public static readonly BindableProperty DisplayValueProperty = BindableProperty.Create(
nameof(DisplayValue), typeof(string), typeof(SettingChooserItemView), null, BindingMode.OneWay);
public static readonly BindableProperty ChooseCommandProperty = BindableProperty.Create(
nameof(ChooseCommand), typeof(ICommand), typeof(ExternalLinkItemView));
public string DisplayValue
{
get { return (string)GetValue(DisplayValueProperty); }
set { SetValue(DisplayValueProperty, value); }
}
public SettingChooserItemView()
{
InitializeComponent();
}
public ICommand ChooseCommand
{
get => GetValue(ChooseCommandProperty) as ICommand;
set => SetValue(ChooseCommandProperty, value);
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<controls:BaseSettingItemView
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.SwitchItemView"
x:Name="_contentView"
ControlTemplate="{StaticResource SettingControlTemplate}">
<controls:BaseSettingItemView.GestureRecognizers>
<TapGestureRecognizer Tapped="ContentView_Tapped" />
</controls:BaseSettingItemView.GestureRecognizers>
<Switch
x:Name="_switch"
HeightRequest="20"
Scale="{OnPlatform iOS=0.8, Android=1}"
IsToggled="{Binding IsToggled, Mode=TwoWay, Source={x:Reference _contentView}}"
AutomationId="{Binding SwitchAutomationId, Mode=OneWay, Source={x:Reference _contentView}}"/>
</controls:BaseSettingItemView>

View File

@@ -0,0 +1,46 @@
using System.Windows.Input;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Controls
{
public partial class SwitchItemView : BaseSettingItemView
{
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(
nameof(IsToggled), typeof(bool), typeof(SwitchItemView), null, BindingMode.TwoWay);
public static readonly BindableProperty SwitchAutomationIdProperty = BindableProperty.Create(
nameof(SwitchAutomationId), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
public static readonly BindableProperty ToggleSwitchCommandProperty = BindableProperty.Create(
nameof(ToggleSwitchCommand), typeof(ICommand), typeof(ExternalLinkItemView));
public SwitchItemView()
{
InitializeComponent();
}
public bool IsToggled
{
get { return (bool)GetValue(IsToggledProperty); }
set { SetValue(IsToggledProperty, value); }
}
public string SwitchAutomationId
{
get { return (string)GetValue(SwitchAutomationIdProperty); }
set { SetValue(SwitchAutomationIdProperty, value); }
}
public ICommand ToggleSwitchCommand
{
get => GetValue(ToggleSwitchCommandProperty) as ICommand;
set => SetValue(ToggleSwitchCommandProperty, value);
}
void ContentView_Tapped(System.Object sender, System.EventArgs e)
{
_switch.IsToggled = !_switch.IsToggled;
}
}
}

View File

@@ -1,18 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFrameworks>net8.0-android;net8.0-ios</TargetFrameworks>
<RootNamespace>Bit.Core</RootNamespace>
<AssemblyName>BitwardenCore</AssemblyName>
<Configurations>Debug;Release;FDroid</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<UseMaui>true</UseMaui>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'">
<CreatePackage>false</CreatePackage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'">
<CreatePackage>false</CreatePackage>
</PropertyGroup>
<!--<ItemGroup>
<None Remove="Resources\eff_long_word_list.txt" />
<None Remove="Resources\public_suffix_list.dat" />
<None Remove="Microsoft.AppCenter.Crashes" />
@@ -21,30 +24,169 @@
<None Remove="MessagePack" />
<None Remove="MessagePack.MSBuild.Tasks" />
<None Remove="Services\EmailForwarders\" />
</ItemGroup>
<None Remove="Resources\Fonts\" />
<None Remove="Resources\Fonts\bwi-font.ttf" />
<None Remove="Resources\Fonts\MaterialIcons_Regular.ttf" />
<None Remove="Resources\Fonts\RobotoMono_Regular.ttf" />
<None Remove="Effects\" />
<None Remove="Resources\Raw\" />
<None Remove="Pages\" />
<None Remove="Behaviors\" />
<None Remove="Controls\" />
<None Remove="Lists\" />
<None Remove="Resources\Styles\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Utilities\Automation\" />
<None Remove="Utilities\Prompts\" />
<None Remove="Resources\Localization\" />
</ItemGroup>-->
<ItemGroup>
<EmbeddedResource Include="Resources\eff_long_word_list.txt" />
<EmbeddedResource Include="Resources\public_suffix_list.dat" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PCLCrypto" Version="2.0.147" />
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="5.0.2" />
<PackageReference Include="MessagePack" Version="2.4.59" />
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.4.59">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ZXing.Net.Maui.Controls" Version="0.3.0-preview.1" />
<PackageReference Include="MessagePack" Version="2.5.124" />
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" />
<!-- <PackageReference Include="Microsoft.AppCenter.Crashes" Version="5.0.2" /> -->
<PackageReference Include="PCLCrypto" Version="2.0.147" />
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Maui" Version="5.2.0" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="2.88.4-preview.84" />
<PackageReference Include="SkiaSharp.Views.Maui.Controls.Compatibility" Version="2.88.4-preview.84" />
<PackageReference Include="FFImageLoadingCompat.Maui" Version="0.1.1" />
<PackageReference Include="AsyncAwaitBestPractices.MVVM" Version="6.0.6" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<PackageReference Include="ZXing.Net.Maui" Version="0.3.0-preview.1" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
<PackageReference Include="Plugin.CurrentActivity" Version="2.1.0.4" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
<PackageReference Include="Xamarin.Firebase.Messaging" Version="123.1.2.2" />
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
<PackageReference Include="ZXing.Net.Maui" Version="0.3.0-preview.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\Logging\" />
<Folder Include="Attributes\" />
<Folder Include="Services\EmailForwarders\" />
<Folder Include="Resources\Fonts\" />
<Folder Include="Effects\" />
<Folder Include="Resources\Raw\" />
<Folder Include="Pages\" />
<Folder Include="Behaviors\" />
<Folder Include="Controls\" />
<Folder Include="Lists\" />
<Folder Include="Resources\Styles\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Utilities\Automation\" />
<Folder Include="Utilities\Prompts\" />
<Folder Include="Resources\Localization\" />
</ItemGroup>
</Project>
<ItemGroup>
<MauiImage Include="Resources\Images\dotnet_bot.svg">
<BaseSize>168,208</BaseSize>
</MauiImage>
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
<MauiFont Include="Resources\Fonts\*" />
</ItemGroup>
<!--<ItemGroup>
<MauiXaml Update="Pages\Accounts\DeleteAccountPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\EnvironmentPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\HintPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\HomePage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\LockPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\LoginApproveDevicePage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\LoginPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\LoginPasswordlessPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\LoginPasswordlessRequestPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\LoginSsoPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\RegisterPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\RemoveMasterPasswordPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\SetPasswordPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\TwoFactorPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\UpdateTempPasswordPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Accounts\VerificationCodePage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Generator\GeneratorHistoryPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Generator\GeneratorPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Send\SendAddEditPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Send\SendAddOnlyOptionsView.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Send\SendAddOnlyPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Send\SendGroupingsPage\SendGroupingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Send\SendsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\AboutSettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\AppearanceSettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\AutofillPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\AutofillSettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\BlockAutofillUrisPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\ExportVaultPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\ExtensionPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\FolderAddEditPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\FoldersPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\LoginPasswordlessRequestsListPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\OtherSettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\SecuritySettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\SettingsPage\SettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Settings\VaultSettingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\AttachmentsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\CipherAddEditPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\CipherDetailsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\CipherSelectionPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\CiphersPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\CollectionsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\GroupingsPage\GroupingsPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\PasswordHistoryPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\ScanPage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Pages\Vault\SharePage.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\AccountSwitchingOverlay\AccountSwitchingOverlayView.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\AccountViewCell\AccountViewCell.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\AuthenticatorViewCell\AuthenticatorViewCell.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\CipherViewCell\CipherViewCell.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\DateTime\DateTimePicker.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\ExternalLinkItemView.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\IconLabelButton\IconLabelButton.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\PasswordStrengthProgressBar\PasswordStrengthProgressBar.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\SendViewCell\SendViewCell.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\Settings\SettingChooserItemView.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Controls\Settings\SwitchItemView.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Lists\ItemLayouts\CustomFields\BooleanCustomFieldItemLayout.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Lists\ItemLayouts\CustomFields\HiddenCustomFieldItemLayout.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Lists\ItemLayouts\CustomFields\LinkedCustomFieldItemLayout.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Lists\ItemLayouts\CustomFields\TextCustomFieldItemLayout.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Android.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Base.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Black.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\ControlTemplates.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Dark.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\iOS.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Light.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Nord.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\SolarizedDark.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
<MauiXaml Update="Resources\Styles\Variables.xaml" Condition=" '$(EnableDefaultXamlItems)' == 'true' " />
</ItemGroup>-->
<ItemGroup>
<EmbeddedResource Update="Resources\Localization\AppResources.resx">
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<Compile Update="Resources\Localization\AppResources.Designer.cs">
<DependentUpon>AppResources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Bit.App.Effects
{
public class FabShadowEffect : RoutingEffect
{
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Platform;
#if ANDROID
using Android.Widget;
#endif
namespace Bit.App.Effects
{
public class FixedSizeEffect : RoutingEffect
{
}
#if ANDROID
public class FixedSizePlatformEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Element is Label label && Control is TextView textView)
{
textView.SetTextSize(Android.Util.ComplexUnitType.Sp, (float)label.FontSize);
}
}
protected override void OnDetached()
{
}
}
#endif
}

View File

@@ -0,0 +1,32 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Platform;
#if ANDROID
using Android.Widget;
#endif
namespace Bit.App.Effects
{
public class NoEmojiKeyboardEffect : RoutingEffect
{
}
#if ANDROID
public class NoEmojiKeyboardPlatformEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Control is EditText editText)
{
editText.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
}
}
protected override void OnDetached()
{
}
}
#endif
}

View File

@@ -0,0 +1,32 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Platform;
#if ANDROID
using Android.Widget;
#endif
namespace Bit.App.Effects
{
public class RemoveFontPaddingEffect : RoutingEffect
{
}
#if ANDROID
public class RemoveFontPaddingPlatformEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Control is TextView textView)
{
textView.SetIncludeFontPadding(false);
}
}
protected override void OnDetached()
{
}
}
#endif
}

View File

@@ -0,0 +1,49 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Platform;
#if IOS
using UIKit;
#endif
namespace Bit.App.Effects
{
public class ScrollEnabledEffect : RoutingEffect
{
public static readonly BindableProperty IsScrollEnabledProperty =
BindableProperty.CreateAttached("IsScrollEnabled", typeof(bool), typeof(ScrollEnabledEffect), true);
public static bool GetIsScrollEnabled(BindableObject view)
{
return (bool)view.GetValue(IsScrollEnabledProperty);
}
public static void SetIsScrollEnabled(BindableObject view, bool value)
{
view.SetValue(IsScrollEnabledProperty, value);
}
public ScrollEnabledEffect()
: base("Bitwarden.ScrollEnabledEffect")
{
}
}
#if IOS
public class ScrollEnabledPlatformEffect : PlatformEffect
{
protected override void OnAttached()
{
// this can be for any view that inherits from UIScrollView like UITextView.
if (Element != null && Control is UIScrollView scrollView)
{
scrollView.ScrollEnabled = Bit.App.Effects.ScrollEnabledEffect.GetIsScrollEnabled(Element);
}
}
protected override void OnDetached()
{
}
}
#endif
}

View File

@@ -0,0 +1,70 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Platform;
#if IOS
using UIKit;
#endif
namespace Bit.App.Effects
{
public enum ScrollContentInsetAdjustmentBehavior
{
Automatic,
ScrollableAxes,
Never,
Always
}
public class ScrollViewContentInsetAdjustmentBehaviorEffect : RoutingEffect
{
public static readonly BindableProperty ContentInsetAdjustmentBehaviorProperty =
BindableProperty.CreateAttached("ContentInsetAdjustmentBehavior", typeof(ScrollContentInsetAdjustmentBehavior), typeof(ScrollViewContentInsetAdjustmentBehaviorEffect), ScrollContentInsetAdjustmentBehavior.Automatic);
public static ScrollContentInsetAdjustmentBehavior GetContentInsetAdjustmentBehavior(BindableObject view)
{
return (ScrollContentInsetAdjustmentBehavior)view.GetValue(ContentInsetAdjustmentBehaviorProperty);
}
public static void SetContentInsetAdjustmentBehavior(BindableObject view, ScrollContentInsetAdjustmentBehavior value)
{
view.SetValue(ContentInsetAdjustmentBehaviorProperty, value);
}
public ScrollViewContentInsetAdjustmentBehaviorEffect()
: base($"Bitwarden.{nameof(ScrollViewContentInsetAdjustmentBehaviorEffect)}")
{
}
}
#if IOS
public class ScrollViewContentInsetAdjustmentBehaviorPlatformEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Element != null && Control is UIScrollView scrollView)
{
switch (Bit.App.Effects.ScrollViewContentInsetAdjustmentBehaviorEffect.GetContentInsetAdjustmentBehavior(Element))
{
case Bit.App.Effects.ScrollContentInsetAdjustmentBehavior.Automatic:
scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Automatic;
break;
case Bit.App.Effects.ScrollContentInsetAdjustmentBehavior.ScrollableAxes:
scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.ScrollableAxes;
break;
case Bit.App.Effects.ScrollContentInsetAdjustmentBehavior.Never:
scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
break;
case Bit.App.Effects.ScrollContentInsetAdjustmentBehavior.Always:
scrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Always;
break;
}
}
}
protected override void OnDetached()
{
}
}
#endif
}

View File

@@ -0,0 +1,34 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Platform;
namespace Bit.App.Effects
{
public class TabBarEffect : RoutingEffect
{
}
#if ANDROID
public class TabBarPlatformEffect : PlatformEffect
{
protected override void OnAttached()
{
// TODO: [MAUI-Migration] [Critical]
// now Container is View instead of ViewGroup, let's review this
//if (!(Container.GetChildAt(0) is ViewGroup layout))
//{
// return;
//}
//if (!(layout.GetChildAt(1) is BottomNavigationView bottomNavigationView))
//{
// return;
//}
//bottomNavigationView.LabelVisibilityMode = LabelVisibilityMode.LabelVisibilityLabeled;
}
protected override void OnDetached()
{
}
}
#endif
}

View File

@@ -0,0 +1,29 @@
using Bit.App.Lists.ItemViewModels.CustomFields;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.DataTemplateSelectors
{
public class CustomFieldItemTemplateSelector : DataTemplateSelector
{
public DataTemplate TextTemplate { get; set; }
public DataTemplate BooleanTemplate { get; set; }
public DataTemplate LinkedTemplate { get; set; }
public DataTemplate HiddenTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
switch (item)
{
case BooleanCustomFieldItemViewModel _:
return BooleanTemplate;
case LinkedCustomFieldItemViewModel _:
return LinkedTemplate;
case HiddenCustomFieldItemViewModel _:
return HiddenTemplate;
default:
return TextTemplate;
}
}
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" ?>
<StackLayout
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.BooleanCustomFieldItemLayout"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core"
x:DataType="cfvm:BooleanCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="BooleanCustomFieldNameLabel" />
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsEditing}"
StyleClass="box-value"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2" />
<controls:IconLabel
Text="{Binding BooleanValue, Mode=OneWay, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Checkbox}}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
Margin="0, 5, 0, 0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="BooleanCustomFieldValueLabel" />
<Switch
IsToggled="{Binding BooleanValue}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationId="BooleanCustomFieldValueToggle" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>

View File

@@ -0,0 +1,13 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class BooleanCustomFieldItemLayout : StackLayout
{
public BooleanCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.HiddenCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core"
x:DataType="cfvm:HiddenCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="HiddenCustomFieldNameLabel" />
<StackLayout
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Converter={StaticResource inverseBool}}">
<controls:MonoLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue}"
AutomationId="HiddenCustomFieldValueLabel" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:MonoEntry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing}"
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding ShowViewHidden}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{Binding Field.Name}"
AutomationId="HiddenCustomFieldValueEntry">
<Entry.Keyboard>
<Keyboard x:FactoryMethod="Create">
<x:Arguments>
<KeyboardFlags>None</KeyboardFlags>
</x:Arguments>
</Keyboard>
</Entry.Keyboard>
</controls:MonoEntry>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValue, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n ToggleVisibility}"
AutomationId="HiddenCustomFieldShowValueButton" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyFieldCommand}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Copy}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@@ -0,0 +1,13 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class HiddenCustomFieldItemLayout : StackLayout
{
public HiddenCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.LinkedCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core"
x:DataType="cfvm:LinkedCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="LinkedCustomFieldNameLabel" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="LinkedCustomFieldValueLabel" />
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsEditing}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value"
AutomationId="LinkedCustomFieldValuePicker" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Options}"
AutomationId="LinkedCustomFieldOptionsButton" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@@ -0,0 +1,13 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class LinkedCustomFieldItemLayout : StackLayout
{
public LinkedCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.TextCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core"
x:DataType="cfvm:TextCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="TextCustomFieldNameLabel" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="TextCustomFieldValueLabel" />
<Entry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing}"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{Binding Field.Name}"
AutomationId="TextCustomFieldValueEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyFieldCommand}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Copy}"
AutomationId="TextCustomFieldCopyValue" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
SemanticProperties.Description="{u:I18n Options}"
AutomationId="TextCustomFieldOptionsButton" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@@ -0,0 +1,13 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class TextCustomFieldItemLayout : StackLayout
{
public TextCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,50 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public abstract class BaseCustomFieldItemViewModel : ExtendedViewModel, ICustomFieldItemViewModel
{
protected FieldView _field;
protected bool _isEditing;
private string[] _additionalFieldProperties = new string[]
{
nameof(ValueText),
nameof(ShowCopyButton)
};
public BaseCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
{
_field = field;
_isEditing = isEditing;
FieldOptionsCommand = new Command(() => fieldOptionsCommand?.Execute(this));
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value,
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(ShowCopyButton),
});
}
public bool IsEditing => _isEditing;
public virtual bool ShowCopyButton => false;
public virtual string ValueText => _field.Value;
public ICommand FieldOptionsCommand { get; }
public void TriggerFieldChanged()
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Windows.Input;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class BooleanCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
public BooleanCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
: base(field, isEditing, fieldOptionsCommand)
{
}
public bool BooleanValue
{
get => bool.TryParse(Field.Value, out var boolVal) && boolVal;
set
{
Field.Value = value.ToString().ToLower();
TriggerPropertyChanged(nameof(BooleanValue));
}
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public interface ICustomFieldItemFactory
{
ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
bool isEditing,
CipherView cipher,
IPasswordPromptable passwordPromptable,
ICommand copyFieldCommand,
ICommand fieldOptionsCommand);
}
public class CustomFieldItemFactory : ICustomFieldItemFactory
{
readonly II18nService _i18nService;
readonly IEventService _eventService;
public CustomFieldItemFactory(II18nService i18nService, IEventService eventService)
{
_i18nService = i18nService;
_eventService = eventService;
}
public ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
bool isEditing,
CipherView cipher,
IPasswordPromptable passwordPromptable,
ICommand copyFieldCommand,
ICommand fieldOptionsCommand)
{
switch (field.Type)
{
case FieldType.Text:
return new TextCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, copyFieldCommand);
case FieldType.Boolean:
return new BooleanCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand);
case FieldType.Hidden:
return new HiddenCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, passwordPromptable, _eventService, copyFieldCommand);
case FieldType.Linked:
return new LinkedCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, _i18nService);
default:
throw new NotImplementedException("There is no custom field item for field type " + field.Type);
}
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class HiddenCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
private readonly CipherView _cipher;
private readonly IPasswordPromptable _passwordPromptable;
private readonly IEventService _eventService;
private bool _showHiddenValue;
public HiddenCustomFieldItemViewModel(FieldView field,
bool isEditing,
ICommand fieldOptionsCommand,
CipherView cipher,
IPasswordPromptable passwordPromptable,
IEventService eventService,
ICommand copyFieldCommand)
: base(field, isEditing, fieldOptionsCommand)
{
_cipher = cipher;
_passwordPromptable = passwordPromptable;
_eventService = eventService;
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
ToggleHiddenValueCommand = new AsyncCommand(ToggleHiddenValueAsync, null, ex =>
{
//#if !FDROID
// Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
//#endif
});
}
public ICommand CopyFieldCommand { get; }
public ICommand ToggleHiddenValueCommand { get; set; }
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value);
}
public bool ShowViewHidden => _cipher.ViewPassword || (_isEditing && _field.NewField);
public override bool ShowCopyButton => !_isEditing && _cipher.ViewPassword && !string.IsNullOrWhiteSpace(Field.Value);
public async Task ToggleHiddenValueAsync()
{
if (!_isEditing && !await _passwordPromptable.PromptPasswordAsync())
{
return;
}
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue && (!_isEditing || _cipher?.Id != null))
{
await _eventService.CollectAsync(
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
}
}

View File

@@ -0,0 +1,13 @@
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public interface ICustomFieldItemViewModel
{
FieldView Field { get; set; }
bool ShowCopyButton { get; }
void TriggerFieldChanged();
}
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class LinkedCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
private readonly CipherView _cipher;
private readonly II18nService _i18nService;
private int _linkedFieldOptionSelectedIndex;
public LinkedCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, CipherView cipher, II18nService i18nService)
: base(field, isEditing, fieldOptionsCommand)
{
_cipher = cipher;
_i18nService = i18nService;
LinkedFieldOptionSelectedIndex = Field.LinkedId.HasValue
? LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value)
: 0;
if (isEditing && Field.LinkedId is null)
{
field.LinkedId = LinkedFieldOptions[0].Value;
}
}
public override string ValueText
{
get
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return $"{BitwardenIcons.Link} {_i18nService.T(i18nKey)}";
}
}
public int LinkedFieldOptionSelectedIndex
{
get => _linkedFieldOptionSelectedIndex;
set
{
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
{
LinkedFieldValueChanged();
}
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => _cipher.LinkedFieldOptions
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
.ToList();
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class TextCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
public TextCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, ICommand copyFieldCommand)
: base(field, isEditing, fieldOptionsCommand)
{
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
}
public override bool ShowCopyButton => !_isEditing && !string.IsNullOrWhiteSpace(Field.Value);
public ICommand CopyFieldCommand { get; }
}
}

50
src/Core/MauiProgram.cs Normal file
View File

@@ -0,0 +1,50 @@
using CommunityToolkit.Maui;
using FFImageLoading.Maui;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Compatibility.Hosting;
using SkiaSharp.Views.Maui.Controls.Hosting;
using ZXing.Net.Maui.Controls;
using AppEffects = Bit.App.Effects;
namespace Bit.Core;
public static class MauiProgram
{
public static MauiAppBuilder ConfigureMauiAppBuilder(Action<IEffectsBuilder> customEffectsBuilder, Action<IMauiHandlersCollection> customHandlers)
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<Bit.App.App>()
.UseMauiCommunityToolkit()
.UseMauiCompatibility()
.UseBarcodeReader()
.UseSkiaSharp()
.UseFFImageLoading()
.ConfigureEffects(effects =>
{
#if ANDROID
effects.Add<AppEffects.FixedSizeEffect, AppEffects.FixedSizePlatformEffect>();
effects.Add<AppEffects.NoEmojiKeyboardEffect, AppEffects.NoEmojiKeyboardPlatformEffect>();
effects.Add<AppEffects.TabBarEffect, AppEffects.TabBarPlatformEffect>();
effects.Add<AppEffects.RemoveFontPaddingEffect, AppEffects.RemoveFontPaddingPlatformEffect>();
#endif
customEffectsBuilder?.Invoke(effects);
})
.ConfigureFonts(fonts =>
{
fonts.AddFont("RobotoMono_Regular.ttf#Roboto Mono", "RobotoMono_Regular");
fonts.AddFont("bwi-font.ttf#bwi-font", "bwi-font");
fonts.AddFont("MaterialIcons_Regular.ttf#Material Icons", "MaterialIcons_Regular");
})
.ConfigureMauiHandlers(handlers =>
{
customHandlers?.Invoke(handlers);
});
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder;
}
}

View File

@@ -0,0 +1,56 @@
using System;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.App.Models
{
public class AppOptions
{
public bool MyVaultTile { get; set; }
public bool GeneratorTile { get; set; }
public bool FromAutofillFramework { get; set; }
public CipherType? FillType { get; set; }
public string Uri { get; set; }
public CipherType? SaveType { get; set; }
public string SaveName { get; set; }
public string SaveUsername { get; set; }
public string SavePassword { get; set; }
public string SaveCardName { get; set; }
public string SaveCardNumber { get; set; }
public string SaveCardExpMonth { get; set; }
public string SaveCardExpYear { get; set; }
public string SaveCardCode { get; set; }
public bool IosExtension { get; set; }
public Tuple<SendType, string, byte[], string> CreateSend { get; set; }
public bool CopyInsteadOfShareAfterSaving { get; set; }
public bool HideAccountSwitcher { get; set; }
public OtpData? OtpData { get; set; }
public void SetAllFrom(AppOptions o)
{
if (o == null)
{
return;
}
MyVaultTile = o.MyVaultTile;
GeneratorTile = o.GeneratorTile;
FromAutofillFramework = o.FromAutofillFramework;
FillType = o.FillType;
Uri = o.Uri;
SaveType = o.SaveType;
SaveName = o.SaveName;
SaveUsername = o.SaveUsername;
SavePassword = o.SavePassword;
SaveCardName = o.SaveCardName;
SaveCardNumber = o.SaveCardNumber;
SaveCardExpMonth = o.SaveCardExpMonth;
SaveCardExpYear = o.SaveCardExpYear;
SaveCardCode = o.SaveCardCode;
IosExtension = o.IosExtension;
CreateSend = o.CreateSend;
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
HideAccountSwitcher = o.HideAccountSwitcher;
OtpData = o.OtpData;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Bit.App.Models
{
public class DialogDetails
{
public string Text { get; set; }
public string Title { get; set; }
public string ConfirmText { get; set; }
public string CancelText { get; set; }
public string Type { get; set; }
public int DialogId { get; set; }
}
}

View File

@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.View;
using CollectionView = Bit.Core.Models.View.CollectionView;
namespace Bit.Core.Models.Domain
{

View File

@@ -1,6 +1,7 @@
using System.Data.Common;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Permissions = Bit.Core.Models.Data.Permissions;
namespace Bit.Core.Models.Domain
{

View File

@@ -1,4 +1,5 @@
using Bit.Core.Models.View;
using CollectionView = Bit.Core.Models.View.CollectionView;
namespace Bit.Core.Models.Export
{

View File

@@ -1,5 +1,6 @@
using Bit.Core.Models.View;
using Newtonsoft.Json;
using CollectionView = Bit.Core.Models.View.CollectionView;
namespace Bit.Core.Models.Export
{

View File

@@ -0,0 +1,23 @@
using System;
namespace Bit.App.Models
{
public abstract class BaseNotificationData
{
public abstract string Type { get; }
public string Id { get; set; }
}
public class PasswordlessNotificationData : BaseNotificationData
{
public const string TYPE = "passwordlessNotificationData";
public override string Type => TYPE;
public int TimeoutInMinutes { get; set; }
public string UserEmail { get; set; }
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace Bit.App.Models
{
public class PlatformCulture
{
public PlatformCulture(string platformCultureString)
{
if (string.IsNullOrWhiteSpace(platformCultureString))
{
throw new ArgumentException("Expected culture identifier.", nameof(platformCultureString));
}
// .NET expects dash, not underscore
PlatformString = platformCultureString.Replace("_", "-");
var dashIndex = PlatformString.IndexOf("-", StringComparison.Ordinal);
if (dashIndex > 0)
{
var parts = PlatformString.Split('-');
LanguageCode = parts[0];
LocaleCode = parts[1];
}
else
{
LanguageCode = PlatformString;
LocaleCode = string.Empty;
}
}
public string PlatformString { get; private set; }
public string LanguageCode { get; private set; }
public string LocaleCode { get; private set; }
public override string ToString()
{
return PlatformString;
}
}
}

Some files were not shown because too many files have changed in this diff Show More