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.

This commit is contained in:
Federico Maccaroni 2024-01-23 17:34:27 -03:00
parent f539bf051d
commit 63904fd303
No known key found for this signature in database
GPG key ID: 5D233F8F2B034536
16 changed files with 350 additions and 400 deletions

View file

@ -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<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
private Task<Stream> 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);
}
}
}
}

View file

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

View file

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

View file

@ -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<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
private Task<Stream> 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);
}
}
}

View file

@ -76,6 +76,7 @@
<Folder Include="Utilities\Prompts\" />
<Folder Include="Resources\Localization\" />
<Folder Include="Controls\Picker\" />
<Folder Include="Controls\Avatar\" />
</ItemGroup>
<ItemGroup>
<MauiImage Include="Resources\Images\dotnet_bot.svg">
@ -105,5 +106,6 @@
</ItemGroup>
<ItemGroup>
<None Remove="Controls\Picker\" />
<None Remove="Controls\Avatar\" />
</ItemGroup>
</Project>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="43">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="43">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -185,20 +185,6 @@
</view>
<toolbarItems/>
<navigationItem key="navigationItem" title="Logins" id="3734">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="3735">
<color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<action selector="CancelBarButton_Activated:" destination="2304" id="3750"/>
</connections>
</barButtonItem>
<barButtonItem title="Account" image="person.2" catalog="system" style="plain" id="I0b-et-FGw" userLabel="Accoutn Switching Button">
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<connections>
<action selector="AccountSwitchingBarButton_Activated:" destination="2304" id="dZn-bd-bC6"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<rightBarButtonItems>
<barButtonItem systemItem="add" id="3736">
<color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -216,9 +202,7 @@
</navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="AccountSwitchingBarButton" destination="I0b-et-FGw" id="KZj-EO-7wd"/>
<outlet property="AddBarButton" destination="3736" id="name-outlet-3736"/>
<outlet property="CancelBarButton" destination="3735" id="name-outlet-3735"/>
<outlet property="MainView" destination="q9o-3n-3xL" id="gjJ-12-71Q"/>
<outlet property="NavItem" destination="3734" id="name-outlet-3734"/>
<outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/>
@ -410,19 +394,6 @@
</constraints>
</view>
<navigationItem key="navigationItem" title="Verify Master Password" id="NCb-RV-Vqq">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="Xoh-Zv-hhd">
<connections>
<action selector="CancelButton_Activated:" destination="cn5-F4-59n" id="1gM-mE-phn"/>
</connections>
</barButtonItem>
<barButtonItem title="Account" image="person.2" catalog="system" style="plain" id="nwd-aM-kFD" userLabel="Accoutn Switching Button">
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<connections>
<action selector="AccountSwitchingBarButton_Activated:" destination="cn5-F4-59n" id="vVZ-IM-rkU"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" title="Submit" id="gju-yD-EmI">
<connections>
<action selector="SubmitButton_Activated:" destination="cn5-F4-59n" id="O1U-fk-BDh"/>
@ -430,8 +401,6 @@
</barButtonItem>
</navigationItem>
<connections>
<outlet property="AccountSwitchingBarButton" destination="nwd-aM-kFD" id="T8F-CN-2il"/>
<outlet property="CancelButton" destination="Xoh-Zv-hhd" id="mwi-4K-maj"/>
<outlet property="MainTableView" destination="FcI-Ph-m9e" id="Ybv-5r-VGA"/>
<outlet property="NavItem" destination="NCb-RV-Vqq" id="L9b-At-x0A"/>
<outlet property="OverlayView" destination="sDX-BN-qLw" id="veu-q4-CeW"/>
@ -601,13 +570,12 @@
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="12959"/>
<segue reference="12574"/>
<segue reference="3731"/>
</inferredMetricsTieBreakers>
<resources>
<image name="check.png" width="90" height="90"/>
<image name="logo.png" width="282" height="44"/>
<image name="person.2" catalog="system" width="128" height="87"/>
<systemColor name="darkTextColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>

View file

@ -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<AvatarInfo?>(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<UIControl> 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();
}
}
}
}

View file

@ -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
{
/// <summary>
/// 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
/// </summary>
public static async Task<UIImage> GetNativeImageAsync(this ImageSource source, CancellationToken cancellationToken = default(CancellationToken))
{
if (source == null || source.IsEmpty)
{
return null;
}
var handler = Microsoft.Maui.Controls.Internals.Registrar.Registered.GetHandlerForObject<IImageSourceHandler>(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;
}
}
}

View file

@ -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> _accountSwitchingOverlayHelper = new Lazy<AccountSwitchingOverlayHelper>(() => 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)
{

View file

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

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="2vH-Do-uhk">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="2vH-Do-uhk">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -14,11 +14,11 @@
<objects>
<viewController id="bHU-LX-EpF" customClass="LoadingViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="z2O-Vp-jY9">
<rect key="frame" x="0.0" y="0.0" width="414" height="808"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="804"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="Zdy-yw-n0p">
<rect key="frame" x="66" y="352" width="282" height="44"/>
<rect key="frame" x="66" y="350" width="282" height="44"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="jNx-Vd-K6U"/>
@ -41,7 +41,7 @@
<objects>
<navigationController definesPresentationContext="YES" id="2vH-Do-uhk" customClass="ExtensionNavigationController" sceneMemberID="viewController">
<navigationBar key="navigationBar" hidden="YES" contentMode="scaleToFill" translucent="NO" id="JoO-jQ-16M">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</navigationBar>
<connections>
@ -61,30 +61,17 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="M1A-84-x5l">
<rect key="frame" x="0.0" y="88" width="414" height="774"/>
<rect key="frame" x="0.0" y="92" width="414" height="770"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ijE-Pa-OBq" userLabel="OverlayView">
<rect key="frame" x="0.0" y="88" width="414" height="774"/>
<rect key="frame" x="0.0" y="92" width="414" height="770"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fav-Fz-6ZK">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<items>
<navigationItem title="Verify Master Password" id="aka-In-IYk">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="LrG-Qx-w4Q">
<connections>
<action selector="CancelButton_Activated:" destination="Vi7-LV-nWW" id="qyZ-i9-Dwz"/>
</connections>
</barButtonItem>
<barButtonItem title="Item" image="person.2" catalog="system" style="plain" id="nlD-Xn-HtM" userLabel="Account Switching Button">
<color key="tintColor" systemColor="systemBackgroundColor"/>
<connections>
<action selector="AccountSwitchingButton_Activated:" destination="Vi7-LV-nWW" id="G3U-rv-UOl"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" title="Submit" id="oQD-QK-YPB">
<connections>
<action selector="SubmitButton_Activated:" destination="Vi7-LV-nWW" id="DgO-TS-MPf"/>
@ -116,8 +103,6 @@
</constraints>
</view>
<connections>
<outlet property="_accountSwitchingButton" destination="nlD-Xn-HtM" id="SSG-zv-bAc"/>
<outlet property="_cancelButton" destination="LrG-Qx-w4Q" id="aag-ZZ-Ifs"/>
<outlet property="_mainTableView" destination="M1A-84-x5l" id="pA4-ao-Fhu"/>
<outlet property="_navBar" destination="fav-Fz-6ZK" id="Q9p-Dw-ipx"/>
<outlet property="_navItem" destination="aka-In-IYk" id="www-Lt-x1g"/>
@ -132,7 +117,6 @@
</scenes>
<resources>
<image name="logo.png" width="282" height="44"/>
<image name="person.2" catalog="system" width="128" height="81"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>