1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-15 10:25:20 +01:00

Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys

This commit is contained in:
Federico Maccaroni 2024-01-23 18:00:53 -03:00
commit 9d29af36e5
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
18 changed files with 353 additions and 403 deletions

View File

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

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

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
@ -17,7 +17,7 @@
LineBreakMode="TailTruncation" />
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
Text="{Binding Source={x:Static core:BitwardenIcons.ExternalLink}}"
TextColor="{DynamicResource TextColor}"
HorizontalOptions="End"
VerticalOptions="Center"

View File

@ -78,6 +78,7 @@
<Folder Include="Resources\Localization\" />
<Folder Include="Utilities\Fido2\" />
<Folder Include="Controls\Picker\" />
<Folder Include="Controls\Avatar\" />
</ItemGroup>
<ItemGroup>
<MauiImage Include="Resources\Images\dotnet_bot.svg">
@ -108,5 +109,6 @@
<ItemGroup>
<None Remove="Utilities\Fido2\" />
<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>