From 63904fd30337f15790b2cfb6a3edfa0a4dd5afe8 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 23 Jan 2024 17:34:27 -0300 Subject: [PATCH] PM-3350 Fix Avatar toolbar icon on extensions to load properly and to take advantage of using directly SkiaSharp to do the native conversion to UIImage. Also improved the toolbar item so that size is set appropriately. --- src/Core/Controls/Avatar/AvatarImageSource.cs | 62 ++++++ .../{ => Avatar}/AvatarImageSourcePool.cs | 0 src/Core/Controls/Avatar/AvatarInfo.cs | 63 ++++++ .../Controls/Avatar/SKAvatarImageHelper.cs | 63 ++++++ src/Core/Controls/AvatarImageSource.cs | 179 ------------------ src/Core/Core.csproj | 2 + .../LockPasswordViewController.cs | 38 +++- .../LockPasswordViewController.designer.cs | 33 +--- src/iOS.Autofill/LoginListViewController.cs | 44 ++++- .../LoginListViewController.designer.cs | 23 --- src/iOS.Autofill/MainInterface.storyboard | 38 +--- .../AccountSwitchingOverlayHelper.cs | 46 ++++- .../Utilities/ImageSourceExtensions.cs | 47 ----- .../LockPasswordViewController.cs | 48 +++-- .../LockPasswordViewController.designer.cs | 32 +--- .../MainInterface.storyboard | 32 +--- 16 files changed, 350 insertions(+), 400 deletions(-) create mode 100644 src/Core/Controls/Avatar/AvatarImageSource.cs rename src/Core/Controls/{ => Avatar}/AvatarImageSourcePool.cs (100%) create mode 100644 src/Core/Controls/Avatar/AvatarInfo.cs create mode 100644 src/Core/Controls/Avatar/SKAvatarImageHelper.cs delete mode 100644 src/Core/Controls/AvatarImageSource.cs delete mode 100644 src/iOS.Core/Utilities/ImageSourceExtensions.cs diff --git a/src/Core/Controls/Avatar/AvatarImageSource.cs b/src/Core/Controls/Avatar/AvatarImageSource.cs new file mode 100644 index 000000000..66682c7d0 --- /dev/null +++ b/src/Core/Controls/Avatar/AvatarImageSource.cs @@ -0,0 +1,62 @@ +using SkiaSharp; + +namespace Bit.App.Controls +{ + public class AvatarImageSource : StreamImageSource + { + private readonly string _text; + private readonly string _id; + private readonly string _color; + private readonly AvatarInfo _avatarInfo; + + 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; + + //Workaround: [MAUI-Migration] There is currently a bug in MAUI where the actual size of the image is used instead of the size it should occupy in the Toolbar. + //This causes some issues with the position of the icon. As a workaround we make the icon smaller until this is fixed. + //Github issues: https://github.com/dotnet/maui/issues/12359 and https://github.com/dotnet/maui/pull/17120 + _avatarInfo = new AvatarInfo(userId, name, email, color, DeviceInfo.Platform == DevicePlatform.iOS ? 20 : 50); + } + + public override Func> Stream => GetStreamAsync; + + private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) + { + var result = Draw(); + return Task.FromResult(result); + } + + private Stream Draw() + { + using (var img = SKAvatarImageHelper.Draw(_avatarInfo)) + { + var data = img.Encode(SKEncodedImageFormat.Png, 100); + return data?.AsStream(true); + } + } + } +} diff --git a/src/Core/Controls/AvatarImageSourcePool.cs b/src/Core/Controls/Avatar/AvatarImageSourcePool.cs similarity index 100% rename from src/Core/Controls/AvatarImageSourcePool.cs rename to src/Core/Controls/Avatar/AvatarImageSourcePool.cs diff --git a/src/Core/Controls/Avatar/AvatarInfo.cs b/src/Core/Controls/Avatar/AvatarInfo.cs new file mode 100644 index 000000000..0380e9d3d --- /dev/null +++ b/src/Core/Controls/Avatar/AvatarInfo.cs @@ -0,0 +1,63 @@ +using Bit.Core.Utilities; + +#nullable enable + +namespace Bit.App.Controls +{ + public struct AvatarInfo + { + private const string DEFAULT_BACKGROUND_COLOR = "#33ffffff"; + + public AvatarInfo(string? userId = null, string? name = null, string? email = null, string? color = null, int size = 50) + { + Size = size; + var text = string.IsNullOrWhiteSpace(name) ? email : name; + + string? upperCaseText = null; + + if (string.IsNullOrEmpty(text)) + { + CharsToDraw = ".."; + } + else if (text.Length > 1) + { + upperCaseText = text.ToUpper(); + CharsToDraw = GetFirstLetters(upperCaseText, 2); + } + else + { + CharsToDraw = upperCaseText = text.ToUpper(); + } + + BackgroundColor = color ?? CoreHelpers.StringToColor(userId ?? upperCaseText, DEFAULT_BACKGROUND_COLOR); + TextColor = CoreHelpers.TextColorFromBgColor(BackgroundColor); + } + + public string CharsToDraw { get; } + public string BackgroundColor { get; } + public string TextColor { get; } + public int Size { get; } + + private static 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; + } + } +} + diff --git a/src/Core/Controls/Avatar/SKAvatarImageHelper.cs b/src/Core/Controls/Avatar/SKAvatarImageHelper.cs new file mode 100644 index 000000000..3557e9a72 --- /dev/null +++ b/src/Core/Controls/Avatar/SKAvatarImageHelper.cs @@ -0,0 +1,63 @@ +using SkiaSharp; + +namespace Bit.App.Controls +{ + public static class SKAvatarImageHelper + { + public static SKImage Draw(AvatarInfo avatarInfo) + { + using (var bitmap = new SKBitmap(avatarInfo.Size * 2, + avatarInfo.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(avatarInfo.BackgroundColor) + }) + { + 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(avatarInfo.BackgroundColor) + }) + { + 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(avatarInfo.TextColor), + TextSize = textSize, + TextAlign = SKTextAlign.Center, + Typeface = typeface + }) + { + var rect = new SKRect(); + textPaint.MeasureText(avatarInfo.CharsToDraw, ref rect); + canvas.DrawText(avatarInfo.CharsToDraw, midX, midY + rect.Height / 2, textPaint); + + return SKImage.FromBitmap(bitmap); + } + } + } + } + } + } + } +} diff --git a/src/Core/Controls/AvatarImageSource.cs b/src/Core/Controls/AvatarImageSource.cs deleted file mode 100644 index c20d61117..000000000 --- a/src/Core/Controls/AvatarImageSource.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Bit.Core.Utilities; -using SkiaSharp; - -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> Stream => GetStreamAsync; - - private Task GetStreamAsync(CancellationToken userToken = new CancellationToken()) - { - var result = Draw(); - 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; - - //Workaround: [MAUI-Migration] There is currently a bug in MAUI where the actual size of the image is used instead of the size it should occupy in the Toolbar. - //This causes some issues with the position of the icon. As a workaround we make the icon smaller until this is fixed. - //Github issues: https://github.com/dotnet/maui/issues/12359 and https://github.com/dotnet/maui/pull/17120 - if (DeviceInfo.Platform == DevicePlatform.iOS) - { - size = 20; - } - - 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); - } - } -} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 82fad1073..b4abf4463 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -76,6 +76,7 @@ + @@ -105,5 +106,6 @@ + \ No newline at end of file diff --git a/src/iOS.Autofill/LockPasswordViewController.cs b/src/iOS.Autofill/LockPasswordViewController.cs index 7f5df4c7e..0d73e1fff 100644 --- a/src/iOS.Autofill/LockPasswordViewController.cs +++ b/src/iOS.Autofill/LockPasswordViewController.cs @@ -2,12 +2,15 @@ using System; using Bit.App.Controls; using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; +using MapKit; using UIKit; namespace Bit.iOS.Autofill { public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController { + UIBarButtonItem _cancelButton; + UIControl _accountSwitchButton; AccountSwitchingOverlayView _accountSwitchingOverlayView; AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; @@ -23,22 +26,37 @@ namespace Bit.iOS.Autofill public CredentialProviderViewController CPViewController { get; set; } public override UINavigationItem BaseNavItem => NavItem; - public override UIBarButtonItem BaseCancelButton => CancelButton; + public override UIBarButtonItem BaseCancelButton => _cancelButton; public override UIBarButtonItem BaseSubmitButton => SubmitButton; public override Action Success => () => CPViewController.DismissLockAndContinue(); public override Action Cancel => () => CPViewController.CompleteRequest(); public override async void ViewDidLoad() { + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + base.ViewDidLoad(); _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); + _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; + + NavItem.SetLeftBarButtonItems(new UIBarButtonItem[] + { + _cancelButton, + new UIBarButtonItem(_accountSwitchButton) + }, false); _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); } - partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) + private void CancelButton_TouchUpInside(object sender, EventArgs e) + { + Cancel(); + } + + private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e) { _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); } @@ -48,9 +66,19 @@ namespace Bit.iOS.Autofill CheckPasswordAsync().FireAndForget(); } - partial void CancelButton_Activated(UIBarButtonItem sender) + protected override void Dispose(bool disposing) { - Cancel(); + if (disposing) + { + if (_accountSwitchButton != null) + { + _accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton); + + _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside; + } + } + + base.Dispose(disposing); } } } diff --git a/src/iOS.Autofill/LockPasswordViewController.designer.cs b/src/iOS.Autofill/LockPasswordViewController.designer.cs index 4fa4f737e..a82cc209f 100644 --- a/src/iOS.Autofill/LockPasswordViewController.designer.cs +++ b/src/iOS.Autofill/LockPasswordViewController.designer.cs @@ -12,13 +12,6 @@ namespace Bit.iOS.Autofill [Register ("LockPasswordViewController")] partial class LockPasswordViewController { - [Outlet] - UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; } - - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelButton { get; set; } - [Outlet] [GeneratedCode ("iOS Designer", "1.0")] UIKit.UITableView MainTableView { get; set; } @@ -34,27 +27,11 @@ namespace Bit.iOS.Autofill [GeneratedCode ("iOS Designer", "1.0")] UIKit.UIBarButtonItem SubmitButton { get; set; } - [Action ("AccountSwitchingBarButton_Activated:")] - partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelButton_Activated:")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("SubmitButton_Activated:")] partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); void ReleaseDesignerOutlets () { - if (AccountSwitchingBarButton != null) { - AccountSwitchingBarButton.Dispose (); - AccountSwitchingBarButton = null; - } - - if (CancelButton != null) { - CancelButton.Dispose (); - CancelButton = null; - } - if (MainTableView != null) { MainTableView.Dispose (); MainTableView = null; @@ -65,15 +42,15 @@ namespace Bit.iOS.Autofill NavItem = null; } - if (SubmitButton != null) { - SubmitButton.Dispose (); - SubmitButton = null; - } - if (OverlayView != null) { OverlayView.Dispose (); OverlayView = null; } + + if (SubmitButton != null) { + SubmitButton.Dispose (); + SubmitButton = null; + } } } } diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index f53f5cb0c..af17166db 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -17,6 +17,9 @@ namespace Bit.iOS.Autofill { public partial class LoginListViewController : ExtendedUIViewController { + UIBarButtonItem _cancelButton; + UIControl _accountSwitchButton; + public LoginListViewController(IntPtr handle) : base(handle) { @@ -37,12 +40,14 @@ namespace Bit.iOS.Autofill public async override void ViewDidLoad() { + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + base.ViewDidLoad(); SubscribeSyncCompleted(); NavItem.Title = AppResources.Items; - CancelBarButton.Title = AppResources.Cancel; + _cancelButton.Title = AppResources.Cancel; TableView.RowHeight = UITableView.AutomaticDimension; TableView.EstimatedRowHeight = 44; @@ -61,21 +66,29 @@ namespace Bit.iOS.Autofill } _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - AccountSwitchingBarButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); + _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; + + NavItem.SetLeftBarButtonItems(new UIBarButtonItem[] + { + _cancelButton, + new UIBarButtonItem(_accountSwitchButton) + }, false); _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); } - partial void AccountSwitchingBarButton_Activated(UIBarButtonItem sender) - { - _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); - } - - partial void CancelBarButton_Activated(UIBarButtonItem sender) + private void CancelButton_TouchUpInside(object sender, EventArgs e) { Cancel(); } + private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e) + { + _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView); + } + private void Cancel() { CPViewController.CompleteRequest(); @@ -151,6 +164,21 @@ namespace Bit.iOS.Autofill }); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_accountSwitchButton != null) + { + _accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton); + + _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside; + } + } + + base.Dispose(disposing); + } + public class TableSource : ExtensionTableSource { private LoginListViewController _controller; diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs index 8bdd8059c..6451849c8 100644 --- a/src/iOS.Autofill/LoginListViewController.designer.cs +++ b/src/iOS.Autofill/LoginListViewController.designer.cs @@ -12,17 +12,10 @@ namespace Bit.iOS.Autofill [Register ("LoginListViewController")] partial class LoginListViewController { - [Outlet] - UIKit.UIBarButtonItem AccountSwitchingBarButton { get; set; } - [Outlet] [GeneratedCode ("iOS Designer", "1.0")] UIKit.UIBarButtonItem AddBarButton { get; set; } - [Outlet] - [GeneratedCode ("iOS Designer", "1.0")] - UIKit.UIBarButtonItem CancelBarButton { get; set; } - [Outlet] UIKit.UIView MainView { get; set; } @@ -36,15 +29,9 @@ namespace Bit.iOS.Autofill [Outlet] UIKit.UITableView TableView { get; set; } - [Action ("AccountSwitchingBarButton_Activated:")] - partial void AccountSwitchingBarButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("AddBarButton_Activated:")] partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("CancelBarButton_Activated:")] - partial void CancelBarButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("SearchBarButton_Activated:")] partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); @@ -55,11 +42,6 @@ namespace Bit.iOS.Autofill AddBarButton = null; } - if (CancelBarButton != null) { - CancelBarButton.Dispose (); - CancelBarButton = null; - } - if (MainView != null) { MainView.Dispose (); MainView = null; @@ -79,11 +61,6 @@ namespace Bit.iOS.Autofill TableView.Dispose (); TableView = null; } - - if (AccountSwitchingBarButton != null) { - AccountSwitchingBarButton.Dispose (); - AccountSwitchingBarButton = null; - } } } } diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard index 7bc66f197..4521ddbc8 100644 --- a/src/iOS.Autofill/MainInterface.storyboard +++ b/src/iOS.Autofill/MainInterface.storyboard @@ -1,9 +1,9 @@ - + - + @@ -185,20 +185,6 @@ - - - - - - - - - - - - - - @@ -216,9 +202,7 @@ - - @@ -410,19 +394,6 @@ - - - - - - - - - - - - - @@ -430,8 +401,6 @@ - - @@ -601,13 +570,12 @@ - + - diff --git a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs index 7171e9f34..6257a6700 100644 --- a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs +++ b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs @@ -1,7 +1,9 @@ using Bit.App.Controls; using Bit.Core.Abstractions; using Bit.Core.Utilities; +using CoreGraphics; using Microsoft.Maui.Platform; +using SkiaSharp.Views.iOS; using UIKit; namespace Bit.iOS.Core.Utilities @@ -30,12 +32,19 @@ namespace Bit.iOS.Core.Utilities throw new NullReferenceException(nameof(_stateService)); } - var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(), - await _stateService.GetNameAsync(), await _stateService.GetEmailAsync(), - await _stateService.GetAvatarColorAsync()); - using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync()) + var avatarInfo = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null + ? null + : new AvatarInfo(a.Profile.UserId, a.Profile.Name, a.Profile.Email, a.Profile.AvatarColor)); + + if (!avatarInfo.HasValue) { - return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + return UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + } + + using (var avatarUIImage = SKAvatarImageHelper.Draw(avatarInfo.Value)) + { + return avatarUIImage?.ToUIImage()?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) + ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); } } catch (Exception ex) @@ -100,5 +109,32 @@ namespace Bit.iOS.Core.Utilities containerView.UserInteractionEnabled = !overlayVisible; containerView.Subviews[0].UserInteractionEnabled = !overlayVisible; } + + public async Task CreateAccountSwitchToolbarButtonItemCustomViewAsync() + { + const float size = 40f; + var image = await CreateAvatarImageAsync(); + var accountSwitchButton = new UIControl(new CGRect(0, 0, size, size)); + if (image != null) + { + var accountSwitchAvatarImageView = new UIImageView(new CGRect(0, 0, size, size)) + { + Image = image + }; + accountSwitchButton.AddSubview(accountSwitchAvatarImageView); + } + + return accountSwitchButton; + } + + public void DisposeAccountSwitchToolbarButtonItemImage(UIControl accountSwitchButton) + { + if (accountSwitchButton?.Subviews?.FirstOrDefault() is UIImageView accountSwitchImageView && accountSwitchImageView.Image != null) + { + var img = accountSwitchImageView.Image; + accountSwitchImageView.Image = null; + img.Dispose(); + } + } } } diff --git a/src/iOS.Core/Utilities/ImageSourceExtensions.cs b/src/iOS.Core/Utilities/ImageSourceExtensions.cs deleted file mode 100644 index 15f3a7d9d..000000000 --- a/src/iOS.Core/Utilities/ImageSourceExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Bit.Core.Services; -using Microsoft.Maui.Controls.Compatibility.Platform.iOS; -using UIKit; - -namespace Bit.iOS.Core.Utilities -{ - public static class ImageSourceExtensions - { - /// - /// Gets the native image from the ImageSource. - /// Taken from https://github.com/xamarin/Xamarin.Forms/blob/02dee20dfa1365d0104758e534581d1fa5958990/Xamarin.Forms.Platform.iOS/Renderers/ImageElementManager.cs#L264 - /// - public static async Task GetNativeImageAsync(this ImageSource source, CancellationToken cancellationToken = default(CancellationToken)) - { - if (source == null || source.IsEmpty) - { - return null; - } - - var handler = Microsoft.Maui.Controls.Internals.Registrar.Registered.GetHandlerForObject(source); - if (handler == null) - { - LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed cause IImageSourceHandler couldn't be found")); - return null; - } - - try - { - float scale = (float)UIScreen.MainScreen.Scale; - return await handler.LoadImageAsync(source, scale: scale, cancelationToken: cancellationToken); - } - catch (OperationCanceledException) - { - LoggerHelper.LogEvenIfCantBeResolved(new OperationCanceledException("GetNativeImageAsync was cancelled")); - } - catch (Exception ex) - { - LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed", ex)); - } - - return null; - } - } -} diff --git a/src/iOS.ShareExtension/LockPasswordViewController.cs b/src/iOS.ShareExtension/LockPasswordViewController.cs index a068c2ae7..4e468b1fd 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.cs @@ -1,15 +1,17 @@ +using System; using Bit.App.Controls; using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; -using System; using UIKit; namespace Bit.iOS.ShareExtension { public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController { + UIBarButtonItem _cancelButton; + UIControl _accountSwitchButton; AccountSwitchingOverlayView _accountSwitchingOverlayView; - AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; + private Lazy _accountSwitchingOverlayHelper = new Lazy(() => new AccountSwitchingOverlayHelper()); public LockPasswordViewController() { @@ -43,15 +45,33 @@ namespace Bit.iOS.ShareExtension public override async void ViewDidLoad() { + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + base.ViewDidLoad(); _cancelButton.TintColor = ThemeHelpers.NavBarTextColor; _submitButton.TintColor = ThemeHelpers.NavBarTextColor; - _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - _accountSwitchingButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + _accountSwitchButton = await _accountSwitchingOverlayHelper.Value.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); + _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; - _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(_overlayView); + _navItem.SetLeftBarButtonItems(new UIBarButtonItem[] + { + _cancelButton, + new UIBarButtonItem(_accountSwitchButton) + }, false); + + _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.Value.CreateAccountSwitchingOverlayView(_overlayView); + } + + private void CancelButton_TouchUpInside(object sender, EventArgs e) + { + Cancel(); + } + + private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e) + { + _accountSwitchingOverlayHelper.Value.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView); } protected override void UpdateNavigationBarTheme() @@ -59,21 +79,11 @@ namespace Bit.iOS.ShareExtension UpdateNavigationBarTheme(_navBar); } - partial void AccountSwitchingButton_Activated(UIBarButtonItem sender) - { - _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView); - } - partial void SubmitButton_Activated(UIBarButtonItem sender) { CheckPasswordAsync().FireAndForget(); } - partial void CancelButton_Activated(UIBarButtonItem sender) - { - Cancel(); - } - protected override void Dispose(bool disposing) { if (disposing) @@ -82,11 +92,11 @@ namespace Bit.iOS.ShareExtension { TableView.Source?.Dispose(); } - if (_accountSwitchingButton?.Image != null) + if (_accountSwitchButton != null) { - var img = _accountSwitchingButton.Image; - _accountSwitchingButton.Image = null; - img.Dispose(); + _accountSwitchingOverlayHelper.Value.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton); + + _accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside; } if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null) { diff --git a/src/iOS.ShareExtension/LockPasswordViewController.designer.cs b/src/iOS.ShareExtension/LockPasswordViewController.designer.cs index 6be61f9dc..d7ee87dc0 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.designer.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.designer.cs @@ -12,12 +12,6 @@ namespace Bit.iOS.ShareExtension [Register ("LockPasswordViewController")] partial class LockPasswordViewController { - [Outlet] - UIKit.UIBarButtonItem _accountSwitchingButton { get; set; } - - [Outlet] - UIKit.UIBarButtonItem _cancelButton { get; set; } - [Outlet] UIKit.UITableView _mainTableView { get; set; } @@ -33,32 +27,21 @@ namespace Bit.iOS.ShareExtension [Outlet] UIKit.UIBarButtonItem _submitButton { get; set; } - [Action ("AccountSwitchingButton_Activated:")] - partial void AccountSwitchingButton_Activated (UIKit.UIBarButtonItem sender); - - [Action ("CancelButton_Activated:")] - partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); - [Action ("SubmitButton_Activated:")] partial void SubmitButton_Activated (UIKit.UIBarButtonItem sender); void ReleaseDesignerOutlets () { - if (_accountSwitchingButton != null) { - _accountSwitchingButton.Dispose (); - _accountSwitchingButton = null; - } - - if (_cancelButton != null) { - _cancelButton.Dispose (); - _cancelButton = null; - } - if (_mainTableView != null) { _mainTableView.Dispose (); _mainTableView = null; } + if (_navBar != null) { + _navBar.Dispose (); + _navBar = null; + } + if (_navItem != null) { _navItem.Dispose (); _navItem = null; @@ -73,11 +56,6 @@ namespace Bit.iOS.ShareExtension _submitButton.Dispose (); _submitButton = null; } - - if (_navBar != null) { - _navBar.Dispose (); - _navBar = null; - } } } } diff --git a/src/iOS.ShareExtension/MainInterface.storyboard b/src/iOS.ShareExtension/MainInterface.storyboard index 1bde113e1..08210eb34 100644 --- a/src/iOS.ShareExtension/MainInterface.storyboard +++ b/src/iOS.ShareExtension/MainInterface.storyboard @@ -1,9 +1,9 @@ - + - + @@ -14,11 +14,11 @@ - + - + @@ -41,7 +41,7 @@ @@ -61,30 +61,17 @@ - + - + - + - - - - - - - - - - - - - @@ -116,8 +103,6 @@ - - @@ -132,7 +117,6 @@ -