diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f5e1f757..c080d2752 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,7 +147,7 @@ jobs: name: com.x8bit.bitwarden-fdroid.apk - name: Set up Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version: '16.x' 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/Controls/ExternalLinkItemView.xaml b/src/Core/Controls/ExternalLinkItemView.xaml index 8a4252eea..9390d2ebb 100644 --- a/src/Core/Controls/ExternalLinkItemView.xaml +++ b/src/Core/Controls/ExternalLinkItemView.xaml @@ -1,4 +1,4 @@ - + + @@ -108,5 +109,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 96c816846..c84cfcf52 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 @@ -