From 0d417b3eee7f5322fe9a24502aa8111e4645aea5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Apr 2019 23:33:12 -0400 Subject: [PATCH] more device actions --- src/Android/Services/DeviceActionService.cs | 29 +++- src/App/Abstractions/IDeviceActionService.cs | 6 +- src/iOS.Core/Views/Toast.cs | 143 +++++++++++++++++++ src/iOS.Core/iOS.Core.csproj | 1 + src/iOS/Services/DeviceActionService.cs | 100 +++++++++++++ src/iOS/iOS.csproj | 5 +- 6 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 src/iOS.Core/Views/Toast.cs create mode 100644 src/iOS/Services/DeviceActionService.cs diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 9b2bba4ba..3b909a998 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -1,10 +1,13 @@ -using Bit.App.Abstractions; +using System.Threading.Tasks; +using Android.App; +using Bit.App.Abstractions; using Plugin.CurrentActivity; namespace Bit.Droid.Services { public class DeviceActionService : IDeviceActionService { + private ProgressDialog _progressDialog; private Android.Widget.Toast _toast; public void Toast(string text, bool longDuration = false) @@ -31,5 +34,29 @@ namespace Bit.Droid.Services } return launchIntent != null; } + + public async Task ShowLoadingAsync(string text) + { + if(_progressDialog != null) + { + await HideLoadingAsync(); + } + var activity = (MainActivity)CrossCurrentActivity.Current.Activity; + _progressDialog = new ProgressDialog(activity); + _progressDialog.SetMessage(text); + _progressDialog.SetCancelable(false); + _progressDialog.Show(); + } + + public Task HideLoadingAsync() + { + if(_progressDialog != null) + { + _progressDialog.Dismiss(); + _progressDialog.Dispose(); + _progressDialog = null; + } + return Task.FromResult(0); + } } } \ No newline at end of file diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 88e01a465..a2e6b30cc 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -1,8 +1,12 @@ -namespace Bit.App.Abstractions +using System.Threading.Tasks; + +namespace Bit.App.Abstractions { public interface IDeviceActionService { void Toast(string text, bool longDuration = false); bool LaunchApp(string appName); + Task ShowLoadingAsync(string text); + Task HideLoadingAsync(); } } \ No newline at end of file diff --git a/src/iOS.Core/Views/Toast.cs b/src/iOS.Core/Views/Toast.cs new file mode 100644 index 000000000..e076b32dc --- /dev/null +++ b/src/iOS.Core/Views/Toast.cs @@ -0,0 +1,143 @@ +using Foundation; +using System; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class Toast : UIView + { + private NSTimer _dismissTimer; + private NSLayoutConstraint _heightConstraint; + private NSLayoutConstraint _leftMarginConstraint; + private NSLayoutConstraint _rightMarginConstraint; + private NSLayoutConstraint _bottomMarginConstraint; + + public Toast(string text) + : base(CoreGraphics.CGRect.FromLTRB(0, 0, 320, 38)) + { + TranslatesAutoresizingMaskIntoConstraints = false; + BackgroundColor = UIColor.DarkGray.ColorWithAlpha(0.9f); + Layer.CornerRadius = 15; + Layer.MasksToBounds = true; + + MessageLabel = new UILabel + { + TranslatesAutoresizingMaskIntoConstraints = false, + TextColor = UIColor.White, + Font = UIFont.SystemFontOfSize(14), + BackgroundColor = UIColor.Clear, + LineBreakMode = UILineBreakMode.WordWrap, + TextAlignment = UITextAlignment.Center, + Lines = 0, + Text = text + }; + + AddSubview(MessageLabel); + + var hMessageConstraints = NSLayoutConstraint.FromVisualFormat("H:|-5-[messageLabel]-5-|", 0, new NSDictionary(), + NSDictionary.FromObjectsAndKeys(new NSObject[] { MessageLabel }, + new NSObject[] { new NSString("messageLabel") }) + ); + + var vMessageConstraints = NSLayoutConstraint.FromVisualFormat("V:|-0-[messageLabel]-0-|", 0, new NSDictionary(), + NSDictionary.FromObjectsAndKeys(new NSObject[] { MessageLabel }, + new NSObject[] { new NSString("messageLabel") }) + ); + + AddConstraints(hMessageConstraints); + AddConstraints(vMessageConstraints); + + AddGestureRecognizer(new UITapGestureRecognizer(() => Dismiss(false))); + } + + public bool Dismissed { get; set; } + public Action DismissCallback { get; set; } + public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(3); + public UILabel MessageLabel { get; set; } + public nfloat LeftMargin { get; set; } = 5; + public nfloat RightMargin { get; set; } = 5; + public nfloat BottomMargin { get; set; } = 5; + public nfloat Height { get; set; } = 38; + + public void Show() + { + if(Superview != null) + { + return; + } + + _dismissTimer = NSTimer.CreateScheduledTimer(Duration, x => Dismiss()); + LayoutIfNeeded(); + + var localSuperView = UIApplication.SharedApplication.KeyWindow; + if(localSuperView != null) + { + localSuperView.AddSubview(this); + + _heightConstraint = NSLayoutConstraint.Create(this, NSLayoutAttribute.Height, + NSLayoutRelation.GreaterThanOrEqual, null, NSLayoutAttribute.NoAttribute, 1, Height); + + _leftMarginConstraint = NSLayoutConstraint.Create(this, NSLayoutAttribute.Left, NSLayoutRelation.Equal, + localSuperView, NSLayoutAttribute.Left, 1, LeftMargin); + + _rightMarginConstraint = NSLayoutConstraint.Create(this, NSLayoutAttribute.Right, NSLayoutRelation.Equal, + localSuperView, NSLayoutAttribute.Right, 1, -RightMargin); + + _bottomMarginConstraint = NSLayoutConstraint.Create(this, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, + localSuperView, NSLayoutAttribute.Bottom, 1, -BottomMargin); + + // Avoid the "UIView-Encapsulated-Layout-Height" constraint conflicts + // http://stackoverflow.com/questions/25059443/what-is-nslayoutconstraint-uiview-encapsulated-layout-height-and-how-should-i + _leftMarginConstraint.Priority = 999; + _rightMarginConstraint.Priority = 999; + + AddConstraint(_heightConstraint); + localSuperView.AddConstraint(_leftMarginConstraint); + localSuperView.AddConstraint(_rightMarginConstraint); + localSuperView.AddConstraint(_bottomMarginConstraint); + + ShowWithAnimation(); + } + else + { + Console.WriteLine("Toast needs a keyWindows to display."); + } + } + + public void Dismiss(bool animated = true) + { + if(Dismissed) + { + return; + } + + Dismissed = true; + _dismissTimer?.Invalidate(); + _dismissTimer = null; + + if(!animated) + { + RemoveFromSuperview(); + DismissCallback?.Invoke(); + return; + } + + SetNeedsLayout(); + Animate(0.3f, 0, UIViewAnimationOptions.CurveEaseIn, () => { Alpha = 0; }, () => + { + RemoveFromSuperview(); + DismissCallback?.Invoke(); + }); + } + + private void ShowWithAnimation() + { + Alpha = 0; + SetNeedsLayout(); + _bottomMarginConstraint.Constant = -BottomMargin; + _leftMarginConstraint.Constant = LeftMargin; + _rightMarginConstraint.Constant = -RightMargin; + AnimateNotify(0.3f, 0, 0.7f, 5f, UIViewAnimationOptions.CurveEaseInOut, () => { Alpha = 1; }, null); + } + } +} diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index a33f2f3e9..89667d36b 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -49,6 +49,7 @@ + diff --git a/src/iOS/Services/DeviceActionService.cs b/src/iOS/Services/DeviceActionService.cs new file mode 100644 index 000000000..dc3ee67b8 --- /dev/null +++ b/src/iOS/Services/DeviceActionService.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.iOS.Core.Views; +using CoreGraphics; +using Foundation; +using UIKit; + +namespace Bit.iOS.Services +{ + public class DeviceActionService : IDeviceActionService + { + private Toast _toast; + private UIAlertController _progressAlert; + + public bool LaunchApp(string appName) + { + throw new NotImplementedException(); + } + + public void Toast(string text, bool longDuration = false) + { + if(!_toast?.Dismissed ?? false) + { + _toast.Dismiss(false); + } + _toast = new Toast(text) + { + Duration = TimeSpan.FromSeconds(longDuration ? 5 : 3) + }; + if(TabBarVisible()) + { + _toast.BottomMargin = 55; + } + _toast.Show(); + _toast.DismissCallback = () => + { + _toast?.Dispose(); + _toast = null; + }; + } + + public Task ShowLoadingAsync(string text) + { + if(_progressAlert != null) + { + HideLoadingAsync().GetAwaiter().GetResult(); + } + + var result = new TaskCompletionSource(); + + var loadingIndicator = new UIActivityIndicatorView(new CGRect(10, 5, 50, 50)); + loadingIndicator.HidesWhenStopped = true; + loadingIndicator.ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray; + loadingIndicator.StartAnimating(); + + _progressAlert = UIAlertController.Create(null, text, UIAlertControllerStyle.Alert); + _progressAlert.View.TintColor = UIColor.Black; + _progressAlert.View.Add(loadingIndicator); + + var vc = GetPresentedViewController(); + vc?.PresentViewController(_progressAlert, false, () => result.TrySetResult(0)); + return result.Task; + } + + public Task HideLoadingAsync() + { + var result = new TaskCompletionSource(); + if(_progressAlert == null) + { + result.TrySetResult(0); + } + _progressAlert.DismissViewController(false, () => result.TrySetResult(0)); + _progressAlert.Dispose(); + _progressAlert = null; + return result.Task; + } + + private UIViewController GetPresentedViewController() + { + var window = UIApplication.SharedApplication.KeyWindow; + var vc = window.RootViewController; + while(vc.PresentedViewController != null) + { + vc = vc.PresentedViewController; + } + return vc; + } + + private bool TabBarVisible() + { + var vc = GetPresentedViewController(); + return vc != null && (vc is UITabBarController || + (vc.ChildViewControllers?.Any(c => c is UITabBarController) ?? false)); + } + } +} \ No newline at end of file diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index c1a5914e6..9edfa14ae 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -92,6 +92,7 @@ + @@ -171,7 +172,5 @@ false - - - + \ No newline at end of file