diff --git a/src/iOS.Core/Controllers/ExtendedUITableViewController.cs b/src/iOS.Core/Controllers/ExtendedUITableViewController.cs new file mode 100644 index 000000000..6ca6893f1 --- /dev/null +++ b/src/iOS.Core/Controllers/ExtendedUITableViewController.cs @@ -0,0 +1,12 @@ +using System; +using UIKit; + +namespace Bit.iOS.Core.Controllers +{ + public class ExtendedUITableViewController : UITableViewController + { + public ExtendedUITableViewController(IntPtr handle) + : base(handle) + { } + } +} diff --git a/src/iOS.Core/Controllers/ExtendedUIViewController.cs b/src/iOS.Core/Controllers/ExtendedUIViewController.cs new file mode 100644 index 000000000..92ceba74e --- /dev/null +++ b/src/iOS.Core/Controllers/ExtendedUIViewController.cs @@ -0,0 +1,12 @@ +using System; +using UIKit; + +namespace Bit.iOS.Core.Controllers +{ + public class ExtendedUIViewController : UIViewController + { + public ExtendedUIViewController(IntPtr handle) + : base(handle) + { } + } +} diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs new file mode 100644 index 000000000..919f8f01e --- /dev/null +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -0,0 +1,177 @@ +using System; +using UIKit; +using Foundation; +using Bit.iOS.Core.Views; +using Bit.App.Resources; +using Bit.iOS.Core.Utilities; +using Bit.App.Abstractions; +using System.Linq; +using Bit.iOS.Core.Controllers; + +namespace Bit.iOS.Core.Controllers +{ + public abstract class LockPasswordViewController : ExtendedUITableViewController + { + //private IAuthService _authService; + //private ICryptoService _cryptoService; + + public LockPasswordViewController(IntPtr handle) : base(handle) + { } + + public abstract UINavigationItem BaseNavItem { get; } + public abstract UIBarButtonItem BaseCancelButton { get; } + public abstract UIBarButtonItem BaseSubmitButton { get; } + public abstract Action Success { get; } + + public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( + AppResources.MasterPassword, useLabelAsPlaceholder: true); + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public override void ViewDidLoad() + { + // _authService = Resolver.Resolve(); + // _cryptoService = Resolver.Resolve(); + + BaseNavItem.Title = AppResources.VerifyMasterPassword; + BaseCancelButton.Title = AppResources.Cancel; + BaseSubmitButton.Title = AppResources.Submit; + View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); + + var descriptor = UIFontDescriptor.PreferredBody; + + MasterPasswordCell.TextField.SecureTextEntry = true; + MasterPasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Go; + MasterPasswordCell.TextField.ShouldReturn += (UITextField tf) => + { + // CheckPassword(); + return true; + }; + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 70; + TableView.Source = new TableSource(this); + TableView.AllowsSelection = true; + + base.ViewDidLoad(); + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + MasterPasswordCell.TextField.BecomeFirstResponder(); + } + + /* + protected void CheckPassword() + { + if(string.IsNullOrWhiteSpace(MasterPasswordCell.TextField.Text)) + { + var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), AppResources.Ok); + PresentViewController(alert, true, null); + return; + } + + var key = _cryptoService.MakeKeyFromPassword(MasterPasswordCell.TextField.Text, _authService.Email, + _authService.Kdf, _authService.KdfIterations); + if(key.Key.SequenceEqual(_cryptoService.Key.Key)) + { + _appSettingsService.Locked = false; + MasterPasswordCell.TextField.ResignFirstResponder(); + Success(); + } + else + { + // TODO: keep track of invalid attempts and logout? + + var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred, + string.Format(null, AppResources.InvalidMasterPassword), AppResources.Ok, (a) => + { + + MasterPasswordCell.TextField.Text = string.Empty; + MasterPasswordCell.TextField.BecomeFirstResponder(); + }); + + PresentViewController(alert, true, null); + } + } + */ + + public class TableSource : UITableViewSource + { + private LockPasswordViewController _controller; + + public TableSource(LockPasswordViewController controller) + { + _controller = controller; + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + if(indexPath.Section == 0) + { + if(indexPath.Row == 0) + { + return _controller.MasterPasswordCell; + } + } + + return new UITableViewCell(); + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + return UITableView.AutomaticDimension; + } + + public override nint NumberOfSections(UITableView tableView) + { + return 1; + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + if(section == 0) + { + return 1; + } + + return 0; + } + + public override nfloat GetHeightForHeader(UITableView tableView, nint section) + { + return UITableView.AutomaticDimension; + } + + public override string TitleForHeader(UITableView tableView, nint section) + { + return null; + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + tableView.DeselectRow(indexPath, true); + tableView.EndEditing(true); + + var cell = tableView.CellAt(indexPath); + if(cell == null) + { + return; + } + + var selectableCell = cell as ISelectable; + if(selectableCell != null) + { + selectableCell.Select(); + } + } + } + } +} diff --git a/src/iOS.Core/Controllers/LoginAddViewController.cs b/src/iOS.Core/Controllers/LoginAddViewController.cs new file mode 100644 index 000000000..707039175 --- /dev/null +++ b/src/iOS.Core/Controllers/LoginAddViewController.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.App.Resources; +using Bit.iOS.Core.Views; +using Foundation; +using UIKit; +using Bit.iOS.Core.Utilities; +using Bit.iOS.Core.Models; +using System.Threading.Tasks; +using AuthenticationServices; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Bit.Core.Exceptions; + +namespace Bit.iOS.Core.Controllers +{ + public abstract class LoginAddViewController : ExtendedUITableViewController + { + private ICipherService _cipherService; + private IFolderService _folderService; + private IEnumerable _folders; + + public LoginAddViewController(IntPtr handle) : base(handle) + { + } + + public AppExtensionContext Context { get; set; } + public FormEntryTableViewCell NameCell { get; set; } = new FormEntryTableViewCell(AppResources.Name); + public FormEntryTableViewCell UsernameCell { get; set; } = new FormEntryTableViewCell(AppResources.Username); + public FormEntryTableViewCell PasswordCell { get; set; } = new FormEntryTableViewCell(AppResources.Password); + public UITableViewCell GeneratePasswordCell { get; set; } = new UITableViewCell( + UITableViewCellStyle.Subtitle, "GeneratePasswordCell"); + public FormEntryTableViewCell UriCell { get; set; } = new FormEntryTableViewCell(AppResources.URI); + public SwitchTableViewCell FavoriteCell { get; set; } = new SwitchTableViewCell(AppResources.Favorite); + public FormEntryTableViewCell NotesCell { get; set; } = new FormEntryTableViewCell( + useTextView: true, height: 180); + public PickerTableViewCell FolderCell { get; set; } = new PickerTableViewCell(AppResources.Folder); + + public abstract UINavigationItem BaseNavItem { get; } + public abstract UIBarButtonItem BaseCancelButton { get; } + public abstract UIBarButtonItem BaseSaveButton { get; } + public abstract Action Success { get; } + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public override void ViewDidLoad() + { + _cipherService = ServiceContainer.Resolve("cipherService"); + _folderService = ServiceContainer.Resolve("folderService"); + + BaseNavItem.Title = AppResources.AddItem; + BaseCancelButton.Title = AppResources.Cancel; + BaseSaveButton.Title = AppResources.Save; + View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); + + NameCell.TextField.Text = Context?.Uri?.Host ?? string.Empty; + NameCell.TextField.ReturnKeyType = UIReturnKeyType.Next; + NameCell.TextField.ShouldReturn += (UITextField tf) => + { + UsernameCell.TextField.BecomeFirstResponder(); + return true; + }; + + UsernameCell.TextField.AutocapitalizationType = UITextAutocapitalizationType.None; + UsernameCell.TextField.AutocorrectionType = UITextAutocorrectionType.No; + UsernameCell.TextField.SpellCheckingType = UITextSpellCheckingType.No; + UsernameCell.TextField.ReturnKeyType = UIReturnKeyType.Next; + UsernameCell.TextField.ShouldReturn += (UITextField tf) => + { + PasswordCell.TextField.BecomeFirstResponder(); + return true; + }; + + PasswordCell.TextField.SecureTextEntry = true; + PasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Next; + PasswordCell.TextField.ShouldReturn += (UITextField tf) => + { + UriCell.TextField.BecomeFirstResponder(); + return true; + }; + + GeneratePasswordCell.TextLabel.Text = AppResources.GeneratePassword; + GeneratePasswordCell.Accessory = UITableViewCellAccessory.DisclosureIndicator; + + UriCell.TextField.Text = Context?.UrlString ?? string.Empty; + UriCell.TextField.KeyboardType = UIKeyboardType.Url; + UriCell.TextField.ReturnKeyType = UIReturnKeyType.Next; + UriCell.TextField.ShouldReturn += (UITextField tf) => + { + NotesCell.TextView.BecomeFirstResponder(); + return true; + }; + + _folders = _folderService.GetAllDecryptedAsync().GetAwaiter().GetResult(); + var folderNames = _folders.Select(s => s.Name).OrderBy(s => s).ToList(); + folderNames.Insert(0, AppResources.FolderNone); + FolderCell.Items = folderNames; + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 70; + TableView.Source = new TableSource(this); + TableView.AllowsSelection = true; + + base.ViewDidLoad(); + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + } + + protected async Task SaveAsync() + { + /* + if(!_connectivity.IsConnected) + { + AlertNoConnection(); + return; + } + */ + + if(string.IsNullOrWhiteSpace(PasswordCell.TextField.Text)) + { + DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, + AppResources.Password), AppResources.Ok); + return; + } + + if(string.IsNullOrWhiteSpace(NameCell.TextField.Text)) + { + DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, + AppResources.Name), AppResources.Ok); + return; + } + + var cipher = new CipherView + { + Name = string.IsNullOrWhiteSpace(NameCell.TextField.Text) ? null : NameCell.TextField.Text, + Notes = string.IsNullOrWhiteSpace(NotesCell.TextView.Text) ? null : NotesCell.TextView.Text, + Favorite = FavoriteCell.Switch.On, + FolderId = FolderCell.SelectedIndex == 0 ? null : _folders.ElementAtOrDefault(FolderCell.SelectedIndex - 1)?.Id, + Type = Bit.Core.Enums.CipherType.Login, + Login = new LoginView + { + Uris = null, + Username = string.IsNullOrWhiteSpace(UsernameCell.TextField.Text) ? null : UsernameCell.TextField.Text, + Password = string.IsNullOrWhiteSpace(PasswordCell.TextField.Text) ? null : PasswordCell.TextField.Text, + } + }; + + if(!string.IsNullOrWhiteSpace(UriCell.TextField.Text)) + { + cipher.Login.Uris = new List + { + new LoginUriView + { + Uri = UriCell.TextField.Text + } + }; + } + + var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); + PresentViewController(loadingAlert, true, null); + try + { + var cipherDomain = await _cipherService.EncryptAsync(cipher); + await _cipherService.SaveWithServerAsync(cipherDomain); + await loadingAlert.DismissViewControllerAsync(true); + if(await ASHelpers.IdentitiesCanIncremental()) + { + var identity = await ASHelpers.GetCipherIdentityAsync(cipherDomain.Id, _cipherService); + if(identity != null) + { + await ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync( + new ASPasswordCredentialIdentity[] { identity }); + } + } + else + { + await ASHelpers.ReplaceAllIdentities(_cipherService); + } + Success(); + } + catch(ApiException e) + { + DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + } + + public void DisplayAlert(string title, string message, string accept) + { + var alert = Dialogs.CreateAlert(title, message, accept); + PresentViewController(alert, true, null); + } + + private void AlertNoConnection() + { + DisplayAlert(AppResources.InternetConnectionRequiredTitle, + AppResources.InternetConnectionRequiredMessage, AppResources.Ok); + } + + public class TableSource : UITableViewSource + { + private LoginAddViewController _controller; + + public TableSource(LoginAddViewController controller) + { + _controller = controller; + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + if(indexPath.Section == 0) + { + if(indexPath.Row == 0) + { + return _controller.NameCell; + } + else if(indexPath.Row == 1) + { + return _controller.UsernameCell; + } + else if(indexPath.Row == 2) + { + return _controller.PasswordCell; + } + else if(indexPath.Row == 3) + { + return _controller.GeneratePasswordCell; + } + } + else if(indexPath.Section == 1) + { + return _controller.UriCell; + } + else if(indexPath.Section == 2) + { + if(indexPath.Row == 0) + { + return _controller.FolderCell; + } + else if(indexPath.Row == 1) + { + return _controller.FavoriteCell; + } + } + else if(indexPath.Section == 3) + { + return _controller.NotesCell; + } + + return new UITableViewCell(); + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + return UITableView.AutomaticDimension; + } + + public override nint NumberOfSections(UITableView tableView) + { + return 4; + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + if(section == 0) + { + return 4; + } + else if(section == 1) + { + return 1; + } + else if(section == 2) + { + return 2; + } + else + { + return 1; + } + } + + public override nfloat GetHeightForHeader(UITableView tableView, nint section) + { + return section == 0 || section == 3 ? UITableView.AutomaticDimension : 0.00001f; + } + + public override string TitleForHeader(UITableView tableView, nint section) + { + if(section == 0) + { + return AppResources.ItemInformation; + } + else if(section == 3) + { + return AppResources.Notes; + } + + return string.Empty; + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + tableView.DeselectRow(indexPath, true); + tableView.EndEditing(true); + + if(indexPath.Section == 0 && indexPath.Row == 3) + { + _controller.PerformSegue("passwordGeneratorSegue", this); + } + + var cell = tableView.CellAt(indexPath); + if(cell == null) + { + return; + } + + var selectableCell = cell as ISelectable; + if(selectableCell != null) + { + selectableCell.Select(); + } + } + } + } +} diff --git a/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs new file mode 100644 index 000000000..bcee98c0d --- /dev/null +++ b/src/iOS.Core/Controllers/PasswordGeneratorViewController.cs @@ -0,0 +1,335 @@ +using System; +using System.Linq; +using Bit.iOS.Core.Views; +using Bit.iOS.Core.Models; +using Foundation; +using UIKit; +using CoreGraphics; +using Bit.iOS.Core.Utilities; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using System.Threading.Tasks; + +namespace Bit.iOS.Core.Controllers +{ + public abstract class PasswordGeneratorViewController : ExtendedUIViewController + { + private IPasswordGenerationService _passwordGenerationService; + + public PasswordGeneratorViewController(IntPtr handle) + : base(handle) + { } + + public UITableViewController OptionsTableViewController { get; set; } + public SwitchTableViewCell UppercaseCell { get; set; } = new SwitchTableViewCell("A-Z"); + public SwitchTableViewCell LowercaseCell { get; set; } = new SwitchTableViewCell("a-z"); + public SwitchTableViewCell NumbersCell { get; set; } = new SwitchTableViewCell("0-9"); + public SwitchTableViewCell SpecialCell { get; set; } = new SwitchTableViewCell("!@#$%^&*"); + public StepperTableViewCell MinNumbersCell { get; set; } = new StepperTableViewCell(AppResources.MinNumbers, 1, 0, 5, 1); + public StepperTableViewCell MinSpecialCell { get; set; } = new StepperTableViewCell(AppResources.MinSpecial, 1, 0, 5, 1); + public SliderTableViewCell LengthCell { get; set; } = new SliderTableViewCell(AppResources.Length, 10, 5, 64); + + public PasswordGenerationOptions PasswordOptions { get; set; } + public abstract UINavigationItem BaseNavItem { get; } + public abstract UIBarButtonItem BaseCancelButton { get; } + public abstract UIBarButtonItem BaseSelectBarButton { get; } + public abstract UILabel BasePasswordLabel { get; } + + public override void ViewWillAppear(bool animated) + { + UINavigationBar.Appearance.ShadowImage = new UIImage(); + UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default); + base.ViewWillAppear(animated); + } + + public async override void ViewDidLoad() + { + _passwordGenerationService = ServiceContainer.Resolve( + "passwordGenerationService"); + + BaseNavItem.Title = AppResources.PasswordGenerator; + BaseCancelButton.Title = AppResources.Cancel; + BaseSelectBarButton.Title = AppResources.Select; + View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); + + var descriptor = UIFontDescriptor.PreferredBody; + BasePasswordLabel.Font = UIFont.FromName("Menlo-Regular", descriptor.PointSize * 1.3f); + BasePasswordLabel.LineBreakMode = UILineBreakMode.TailTruncation; + BasePasswordLabel.Lines = 1; + BasePasswordLabel.AdjustsFontSizeToFitWidth = false; + + var controller = ChildViewControllers.LastOrDefault(); + if(controller != null) + { + OptionsTableViewController = controller as UITableViewController; + } + + if(OptionsTableViewController != null) + { + OptionsTableViewController.TableView.RowHeight = UITableView.AutomaticDimension; + OptionsTableViewController.TableView.EstimatedRowHeight = 70; + OptionsTableViewController.TableView.Source = new TableSource(this); + OptionsTableViewController.TableView.AllowsSelection = true; + OptionsTableViewController.View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f); + } + + var options = await _passwordGenerationService.GetOptionsAsync(); + UppercaseCell.Switch.On = options.Uppercase.GetValueOrDefault(); + LowercaseCell.Switch.On = options.Lowercase.GetValueOrDefault(true); + SpecialCell.Switch.On = options.Special.GetValueOrDefault(); + NumbersCell.Switch.On = options.Number.GetValueOrDefault(); + MinNumbersCell.Value = options.MinNumber.GetValueOrDefault(1); + MinSpecialCell.Value = options.MinSpecial.GetValueOrDefault(1); + LengthCell.Value = options.Length.GetValueOrDefault(14); + + UppercaseCell.ValueChanged += Options_ValueChanged; + LowercaseCell.ValueChanged += Options_ValueChanged; + NumbersCell.ValueChanged += Options_ValueChanged; + SpecialCell.ValueChanged += Options_ValueChanged; + MinNumbersCell.ValueChanged += Options_ValueChanged; + MinSpecialCell.ValueChanged += Options_ValueChanged; + LengthCell.ValueChanged += Options_ValueChanged; + + // Adjust based on context password options + if(PasswordOptions != null) + { + if(PasswordOptions.RequireDigits) + { + NumbersCell.Switch.On = true; + NumbersCell.Switch.Enabled = false; + + if(MinNumbersCell.Value < 1) + { + MinNumbersCell.Value = 1; + } + + MinNumbersCell.Stepper.MinimumValue = 1; + } + + if(PasswordOptions.RequireSymbols) + { + SpecialCell.Switch.On = true; + SpecialCell.Switch.Enabled = false; + + if(MinSpecialCell.Value < 1) + { + MinSpecialCell.Value = 1; + } + + MinSpecialCell.Stepper.MinimumValue = 1; + } + + if(PasswordOptions.MinLength < PasswordOptions.MaxLength) + { + if(PasswordOptions.MinLength > 0 && PasswordOptions.MinLength > LengthCell.Slider.MinValue) + { + if(LengthCell.Value < PasswordOptions.MinLength) + { + LengthCell.Slider.Value = PasswordOptions.MinLength; + } + + LengthCell.Slider.MinValue = PasswordOptions.MinLength; + } + + if(PasswordOptions.MaxLength > 5 && PasswordOptions.MaxLength < LengthCell.Slider.MaxValue) + { + if(LengthCell.Value > PasswordOptions.MaxLength) + { + LengthCell.Slider.Value = PasswordOptions.MaxLength; + } + + LengthCell.Slider.MaxValue = PasswordOptions.MaxLength; + } + } + } + + var task = GeneratePasswordAsync(); + base.ViewDidLoad(); + } + + private void Options_ValueChanged(object sender, EventArgs e) + { + if(InvalidState()) + { + LowercaseCell.Switch.On = true; + } + var task = GeneratePasswordAsync(); + } + + private bool InvalidState() + { + return !LowercaseCell.Switch.On && !UppercaseCell.Switch.On && !NumbersCell.Switch.On && !SpecialCell.Switch.On; + } + + private async Task GeneratePasswordAsync() + { + BasePasswordLabel.Text = await _passwordGenerationService.GeneratePasswordAsync( + new Bit.Core.Models.Domain.PasswordGenerationOptions + { + Length = LengthCell.Value, + Uppercase = UppercaseCell.Switch.On, + Lowercase = LowercaseCell.Switch.On, + Number = NumbersCell.Switch.On, + Special = SpecialCell.Switch.On, + MinSpecial = MinSpecialCell.Value, + MinNumber = MinNumbersCell.Value, + }); + } + + public class TableSource : UITableViewSource + { + private PasswordGeneratorViewController _controller; + + public TableSource(PasswordGeneratorViewController controller) + { + _controller = controller; + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + if(indexPath.Section == 0) + { + var cell = new UITableViewCell(); + cell.TextLabel.TextColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f); + if(indexPath.Row == 0) + { + cell.TextLabel.Text = AppResources.RegeneratePassword; + } + else if(indexPath.Row == 1) + { + cell.TextLabel.Text = AppResources.CopyPassword; + } + return cell; + } + + if(indexPath.Row == 0) + { + return _controller.LengthCell; + } + else if(indexPath.Row == 1) + { + return _controller.UppercaseCell; + } + else if(indexPath.Row == 2) + { + return _controller.LowercaseCell; + } + else if(indexPath.Row == 3) + { + return _controller.NumbersCell; + } + else if(indexPath.Row == 4) + { + return _controller.SpecialCell; + } + else if(indexPath.Row == 5) + { + return _controller.MinNumbersCell; + } + else if(indexPath.Row == 6) + { + return _controller.MinSpecialCell; + } + + return new UITableViewCell(); + } + + public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) + { + return UITableView.AutomaticDimension; + } + + public override nint NumberOfSections(UITableView tableView) + { + return 2; + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + if(section == 0) + { + return 2; + } + + return 7; + } + + public override nfloat GetHeightForHeader(UITableView tableView, nint section) + { + if(section == 0) + { + return 0.00001f; + } + + return UITableView.AutomaticDimension; + } + + public override UIView GetViewForHeader(UITableView tableView, nint section) + { + if(section == 0) + { + return new UIView(CGRect.Empty) + { + Hidden = true + }; + } + + return null; + } + + public override string TitleForHeader(UITableView tableView, nint section) + { + if(section == 1) + { + return AppResources.Options; + } + + return null; + } + + public override string TitleForFooter(UITableView tableView, nint section) + { + if(section == 1) + { + return AppResources.OptionDefaults; + } + + return null; + } + + public override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + if(indexPath.Section == 0) + { + if(indexPath.Row == 0) + { + var task = _controller.GeneratePasswordAsync(); + } + else if(indexPath.Row == 1) + { + UIPasteboard clipboard = UIPasteboard.General; + clipboard.String = _controller.BasePasswordLabel.Text; + var alert = Dialogs.CreateMessageAlert( + string.Format(AppResources.ValueHasBeenCopied, AppResources.Password)); + _controller.PresentViewController(alert, true, () => + { + _controller.DismissViewController(true, null); + }); + } + } + + tableView.DeselectRow(indexPath, true); + tableView.EndEditing(true); + } + + public NSDate DateTimeToNSDate(DateTime date) + { + DateTime reference = TimeZone.CurrentTimeZone.ToLocalTime( + new DateTime(2001, 1, 1, 0, 0, 0)); + return NSDate.FromTimeIntervalSinceReferenceDate( + (date - reference).TotalSeconds); + } + } + } +} diff --git a/src/iOS.Core/Models/CipherViewModel.cs b/src/iOS.Core/Models/CipherViewModel.cs index 5aa36d466..45b6f8cc9 100644 --- a/src/iOS.Core/Models/CipherViewModel.cs +++ b/src/iOS.Core/Models/CipherViewModel.cs @@ -10,6 +10,7 @@ namespace Bit.iOS.Core.Models { public CipherViewModel(CipherView cipher) { + CipherView = cipher; Id = cipher.Id; Name = cipher.Name; Username = cipher.Login?.Username; @@ -26,6 +27,7 @@ namespace Bit.iOS.Core.Models public List Uris { get; set; } public string Totp { get; set; } public List> Fields { get; set; } + public CipherView CipherView { get; set; } public class LoginUriModel { diff --git a/src/iOS.Core/Views/ExtensionSearchDelegate.cs b/src/iOS.Core/Views/ExtensionSearchDelegate.cs new file mode 100644 index 000000000..317d55138 --- /dev/null +++ b/src/iOS.Core/Views/ExtensionSearchDelegate.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Foundation; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class ExtensionSearchDelegate : UISearchBarDelegate + { + private readonly UITableView _tableView; + private CancellationTokenSource _filterResultsCancellationTokenSource; + + public ExtensionSearchDelegate(UITableView tableView) + { + _tableView = tableView; + } + + public override void TextChanged(UISearchBar searchBar, string searchText) + { + var cts = new CancellationTokenSource(); + Task.Run(() => + { + NSRunLoop.Main.BeginInvokeOnMainThread(async () => + { + if(!string.IsNullOrWhiteSpace(searchText)) + { + await Task.Delay(300); + if(searchText != searchBar.Text) + { + return; + } + else + { + _filterResultsCancellationTokenSource?.Cancel(); + } + } + try + { + ((ExtensionTableSource)_tableView.Source).FilterResults(searchText, cts.Token); + _tableView.ReloadData(); + } + catch(OperationCanceledException) { } + _filterResultsCancellationTokenSource = cts; + }); + }, cts.Token); + } + } +} \ No newline at end of file diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs new file mode 100644 index 000000000..9d3fff2ec --- /dev/null +++ b/src/iOS.Core/Views/ExtensionTableSource.cs @@ -0,0 +1,148 @@ +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Bit.iOS.Core.Models; +using Foundation; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class ExtensionTableSource : UITableViewSource + { + private const string CellIdentifier = "TableCell"; + + private IEnumerable _allItems = new List(); + protected ICipherService _cipherService; + protected ITotpService _totpService; + protected IUserService _userService; + private AppExtensionContext _context; + private UIViewController _controller; + + public ExtensionTableSource(AppExtensionContext context, UIViewController controller) + { + _cipherService = ServiceContainer.Resolve("cipherService"); + _totpService = ServiceContainer.Resolve("totpService"); + _userService = ServiceContainer.Resolve("userService"); + _context = context; + _controller = controller; + } + + public IEnumerable Items { get; private set; } + + public async Task LoadItemsAsync(bool urlFilter = true, string searchFilter = null) + { + var combinedLogins = new List(); + + if(urlFilter) + { + var logins = await _cipherService.GetAllDecryptedByUrlAsync(_context.UrlString); + if(logins?.Item1 != null) + { + combinedLogins.AddRange(logins.Item1); + } + if(logins?.Item2 != null) + { + combinedLogins.AddRange(logins.Item2); + } + } + else + { + var logins = await _cipherService.GetAllDecryptedAsync(); + combinedLogins.AddRange(logins); + } + + _allItems = combinedLogins + .Where(c => c.Type == Bit.Core.Enums.CipherType.Login) + .Select(s => new CipherViewModel(s)) + .OrderBy(s => s.Name) + .ThenBy(s => s.Username) + .ToList() ?? new List(); + FilterResults(searchFilter, new CancellationToken()); + } + + public void FilterResults(string searchFilter, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + if(string.IsNullOrWhiteSpace(searchFilter)) + { + Items = _allItems.ToList(); + } + else + { + searchFilter = searchFilter.ToLower(); + Items = _allItems + .Where(s => s.Name?.ToLower().Contains(searchFilter) ?? false || + (s.Username?.ToLower().Contains(searchFilter) ?? false) || + (s.Uris?.FirstOrDefault()?.Uri?.ToLower().Contains(searchFilter) ?? false)) + .TakeWhile(s => !ct.IsCancellationRequested) + .ToArray(); + } + } + + public IEnumerable TableItems { get; set; } + + public override nint RowsInSection(UITableView tableview, nint section) + { + return Items == null || Items.Count() == 0 ? 1 : Items.Count(); + } + + public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) + { + if(Items == null || Items.Count() == 0) + { + var noDataCell = new UITableViewCell(UITableViewCellStyle.Default, "NoDataCell"); + noDataCell.TextLabel.Text = AppResources.NoItemsTap; + noDataCell.TextLabel.TextAlignment = UITextAlignment.Center; + noDataCell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; + noDataCell.TextLabel.Lines = 0; + return noDataCell; + } + + var cell = tableView.DequeueReusableCell(CellIdentifier); + + // if there are no cells to reuse, create a new one + if(cell == null) + { + Debug.WriteLine("BW Log, Make new cell for list."); + cell = new UITableViewCell(UITableViewCellStyle.Subtitle, CellIdentifier); + cell.DetailTextLabel.TextColor = cell.DetailTextLabel.TintColor = + new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); + } + return cell; + } + + public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath) + { + if(Items == null || Items.Count() == 0 || cell == null) + { + return; + } + + var item = Items.ElementAt(indexPath.Row); + cell.TextLabel.Text = item.Name; + cell.DetailTextLabel.Text = item.Username; + } + + public async Task GetTotpAsync(CipherViewModel item) + { + string totp = null; + var accessPremium = await _userService.CanAccessPremiumAsync(); + if(accessPremium || (item?.CipherView.OrganizationUseTotp ?? false)) + { + if(item != null && !string.IsNullOrWhiteSpace(item.Totp)) + { + totp = await _totpService.GetCodeAsync(item.Totp); + } + } + return totp; + } + } +} diff --git a/src/iOS.Core/Views/FormEntryTableViewCell.cs b/src/iOS.Core/Views/FormEntryTableViewCell.cs new file mode 100644 index 000000000..9088e7e0a --- /dev/null +++ b/src/iOS.Core/Views/FormEntryTableViewCell.cs @@ -0,0 +1,129 @@ +using System; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class FormEntryTableViewCell : UITableViewCell, ISelectable + { + public FormEntryTableViewCell( + string labelName = null, + bool useTextView = false, + nfloat? height = null, + bool useLabelAsPlaceholder = false) + : base(UITableViewCellStyle.Default, nameof(FormEntryTableViewCell)) + { + var descriptor = UIFontDescriptor.PreferredBody; + var pointSize = descriptor.PointSize; + + if(labelName != null && !useLabelAsPlaceholder) + { + Label = new UILabel + { + Text = labelName, + TranslatesAutoresizingMaskIntoConstraints = false, + Font = UIFont.FromDescriptor(descriptor, 0.8f * pointSize), + TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f) + }; + + ContentView.Add(Label); + } + + if(useTextView) + { + TextView = new UITextView + { + TranslatesAutoresizingMaskIntoConstraints = false, + Font = UIFont.FromDescriptor(descriptor, pointSize) + }; + + ContentView.Add(TextView); + ContentView.AddConstraints(new NSLayoutConstraint[] { + NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextView, NSLayoutAttribute.Trailing, 1f, 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextView, NSLayoutAttribute.Bottom, 1f, 10f) + }); + + if(labelName != null && !useLabelAsPlaceholder) + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Bottom, 1f, 10f)); + } + else + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f)); + } + + if(height.HasValue) + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, height.Value)); + } + } + else + { + TextField = new UITextField + { + TranslatesAutoresizingMaskIntoConstraints = false, + BorderStyle = UITextBorderStyle.None, + Font = UIFont.FromDescriptor(descriptor, pointSize), + ClearButtonMode = UITextFieldViewMode.WhileEditing + }; + + if(useLabelAsPlaceholder) + { + TextField.Placeholder = labelName; + } + + ContentView.Add(TextField); + ContentView.AddConstraints(new NSLayoutConstraint[] { + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Bottom, 1f, 10f) + }); + + if(labelName != null && !useLabelAsPlaceholder) + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Bottom, 1f, 10f)); + } + else + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f)); + } + + if(height.HasValue) + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, height.Value)); + } + } + + if(labelName != null && !useLabelAsPlaceholder) + { + ContentView.AddConstraints(new NSLayoutConstraint[] { + NSLayoutConstraint.Create(Label, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), + NSLayoutConstraint.Create(Label, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Trailing, 1f, 15f) + }); + } + } + + public UILabel Label { get; set; } + public UITextField TextField { get; set; } + public UITextView TextView { get; set; } + + public void Select() + { + if(TextView != null) + { + TextView.BecomeFirstResponder(); + } + else if(TextField != null) + { + TextField.BecomeFirstResponder(); + } + } + } +} diff --git a/src/iOS.Core/Views/ISelectable.cs b/src/iOS.Core/Views/ISelectable.cs new file mode 100644 index 000000000..463d783fd --- /dev/null +++ b/src/iOS.Core/Views/ISelectable.cs @@ -0,0 +1,7 @@ +namespace Bit.iOS.Core.Views +{ + public interface ISelectable + { + void Select(); + } +} diff --git a/src/iOS.Core/Views/PickerTableViewCell.cs b/src/iOS.Core/Views/PickerTableViewCell.cs new file mode 100644 index 000000000..84094b752 --- /dev/null +++ b/src/iOS.Core/Views/PickerTableViewCell.cs @@ -0,0 +1,195 @@ +using CoreGraphics; +using System; +using System.Collections.Generic; +using System.Drawing; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class PickerTableViewCell : UITableViewCell, ISelectable + { + private List _items = new List(); + private int _selectedIndex = 0; + + public PickerTableViewCell( + string labelName, + nfloat? height = null) + : base(UITableViewCellStyle.Default, nameof(PickerTableViewCell)) + { + var descriptor = UIFontDescriptor.PreferredBody; + var pointSize = descriptor.PointSize; + + Label = new UILabel + { + Text = labelName, + TranslatesAutoresizingMaskIntoConstraints = false, + Font = UIFont.FromDescriptor(descriptor, 0.8f * pointSize), + TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f) + }; + + ContentView.Add(Label); + + TextField = new NoCaretField + { + BorderStyle = UITextBorderStyle.None, + TranslatesAutoresizingMaskIntoConstraints = false, + Font = UIFont.FromDescriptor(descriptor, pointSize) + }; + + var width = (float)UIScreen.MainScreen.Bounds.Width; + var toolbar = new UIToolbar(new RectangleF(0, 0, width, 44)) + { + BarStyle = UIBarStyle.Default, + Translucent = true + }; + var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace); + var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, a) => + { + var s = (PickerSource)Picker.Model; + if(s.SelectedIndex == -1 && Items != null && Items.Count > 0) + { + UpdatePickerSelectedIndex(0); + } + TextField.Text = s.SelectedItem; + TextField.ResignFirstResponder(); + }); + + toolbar.SetItems(new[] { spacer, doneButton }, false); + + TextField.InputView = Picker; + TextField.InputAccessoryView = toolbar; + + ContentView.Add(TextField); + + ContentView.AddConstraints(new NSLayoutConstraint[] { + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Bottom, 1f, 10f), + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Top, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Bottom, 1f, 10f), + NSLayoutConstraint.Create(Label, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, 15f), + NSLayoutConstraint.Create(Label, NSLayoutAttribute.Top, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Top, 1f, 10f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Label, NSLayoutAttribute.Trailing, 1f, 15f) + }); + + if(height.HasValue) + { + ContentView.AddConstraint( + NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.NoAttribute, 1f, height.Value)); + } + + Picker.Model = new PickerSource(this); + } + + public UITextField TextField { get; set; } + public UILabel Label { get; set; } + public UIPickerView Picker { get; set; } = new UIPickerView(); + + public List Items + { + get { return _items; } + set + { + _items = value; + UpdatePicker(); + } + } + + public int SelectedIndex + { + get { return _selectedIndex; } + set + { + _selectedIndex = value; + UpdatePicker(); + } + } + + public string SelectedItem => TextField.Text; + + private void UpdatePicker() + { + TextField.Text = SelectedIndex == -1 || Items == null ? "" : Items[SelectedIndex]; + Picker.ReloadAllComponents(); + if(Items == null || Items.Count == 0) + { + return; + } + + UpdatePickerSelectedIndex(SelectedIndex); + } + + private void UpdatePickerFromModel(PickerSource s) + { + TextField.Text = s.SelectedItem; + _selectedIndex = s.SelectedIndex; + } + + private void UpdatePickerSelectedIndex(int formsIndex) + { + var source = (PickerSource)Picker.Model; + source.SelectedIndex = formsIndex; + source.SelectedItem = formsIndex >= 0 ? Items[formsIndex] : null; + Picker.Select(Math.Max(formsIndex, 0), 0, true); + } + + public void Select() + { + TextField?.BecomeFirstResponder(); + } + + private class NoCaretField : UITextField + { + public NoCaretField() : base(default(CGRect)) + { } + + public override CGRect GetCaretRectForPosition(UITextPosition position) + { + return default(CGRect); + } + } + + private class PickerSource : UIPickerViewModel + { + private readonly PickerTableViewCell _cell; + + public PickerSource(PickerTableViewCell cell) + { + _cell = cell; + } + + public int SelectedIndex { get; internal set; } + public string SelectedItem { get; internal set; } + + public override nint GetComponentCount(UIPickerView picker) + { + return 1; + } + + public override nint GetRowsInComponent(UIPickerView pickerView, nint component) + { + return _cell.Items != null ? _cell.Items.Count : 0; + } + + public override string GetTitle(UIPickerView picker, nint row, nint component) + { + return _cell.Items[(int)row]; + } + + public override void Selected(UIPickerView picker, nint row, nint component) + { + if(_cell.Items.Count == 0) + { + SelectedItem = null; + SelectedIndex = -1; + } + else + { + SelectedItem = _cell.Items[(int)row]; + SelectedIndex = (int)row; + } + + _cell.UpdatePickerFromModel(this); + } + } + } +} diff --git a/src/iOS.Core/Views/SliderTableViewCell.cs b/src/iOS.Core/Views/SliderTableViewCell.cs new file mode 100644 index 000000000..30231d2dd --- /dev/null +++ b/src/iOS.Core/Views/SliderTableViewCell.cs @@ -0,0 +1,56 @@ +using System; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class SliderTableViewCell : UITableViewCell + { + private string _detailRightSpace = "\t"; + private int _value; + + public SliderTableViewCell(string labelName, int value, int min, int max) + : base(UITableViewCellStyle.Value1, nameof(SwitchTableViewCell)) + { + TextLabel.Text = labelName; + DetailTextLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); + + Slider = new UISlider + { + MinValue = min, + MaxValue = max, + TintColor = new UIColor(red: 0.24f, green: 0.55f, blue: 0.74f, alpha: 1.0f), + Frame = new CoreGraphics.CGRect(0, 0, 180, 30) + }; + Slider.ValueChanged += Slider_ValueChanged; + Value = value; + + AccessoryView = Slider; + } + + private void Slider_ValueChanged(object sender, EventArgs e) + { + var newValue = Convert.ToInt32(Math.Round(Slider.Value, 0)); + bool valueChanged = newValue != Value; + + Value = newValue; + + if(valueChanged) + { + ValueChanged?.Invoke(this, null); + } + } + + public UISlider Slider { get; set; } + public int Value + { + get { return _value; } + set + { + _value = value; + Slider.Value = value; + DetailTextLabel.Text = string.Concat(value.ToString(), _detailRightSpace); + } + } + public event EventHandler ValueChanged; + } +} diff --git a/src/iOS.Core/Views/StepperTableViewCell.cs b/src/iOS.Core/Views/StepperTableViewCell.cs new file mode 100644 index 000000000..02bdf8ce1 --- /dev/null +++ b/src/iOS.Core/Views/StepperTableViewCell.cs @@ -0,0 +1,51 @@ +using System; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class StepperTableViewCell : UITableViewCell + { + // Give some space to the right of the detail in between the spacer. + // This is a bit of a hack, but I did not see a way to specify a margin on the + // detaul DetailTextLabel or AccessoryView + private string _detailRightSpace = "\t"; + private int _value; + + public StepperTableViewCell(string labelName, int value, int min, int max, int increment) + : base(UITableViewCellStyle.Value1, nameof(SwitchTableViewCell)) + { + TextLabel.Text = labelName; + DetailTextLabel.TextColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f); + + Stepper = new UIStepper + { + TintColor = new UIColor(red: 0.47f, green: 0.47f, blue: 0.47f, alpha: 1.0f), + MinimumValue = min, + MaximumValue = max + }; + Stepper.ValueChanged += Stepper_ValueChanged; + Value = value; + + AccessoryView = Stepper; + } + + private void Stepper_ValueChanged(object sender, EventArgs e) + { + Value = Convert.ToInt32(Stepper.Value); + ValueChanged?.Invoke(this, null); + } + + public UIStepper Stepper { get; private set; } + public int Value + { + get { return _value; } + set + { + _value = value; + Stepper.Value = value; + DetailTextLabel.Text = string.Concat(value.ToString(), _detailRightSpace); + } + } + public event EventHandler ValueChanged; + } +} diff --git a/src/iOS.Core/Views/SwitchTableViewCell.cs b/src/iOS.Core/Views/SwitchTableViewCell.cs new file mode 100644 index 000000000..9302f89e4 --- /dev/null +++ b/src/iOS.Core/Views/SwitchTableViewCell.cs @@ -0,0 +1,25 @@ +using System; +using UIKit; + +namespace Bit.iOS.Core.Views +{ + public class SwitchTableViewCell : UITableViewCell + { + public SwitchTableViewCell(string labelName) + : base(UITableViewCellStyle.Default, nameof(SwitchTableViewCell)) + { + TextLabel.Text = labelName; + AccessoryView = Switch; + + Switch.ValueChanged += Switch_ValueChanged; + } + + private void Switch_ValueChanged(object sender, EventArgs e) + { + ValueChanged?.Invoke(this, null); + } + + public UISwitch Switch { get; set; } = new UISwitch(); + public event EventHandler ValueChanged; + } +} diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index e0117a229..436698af3 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -54,6 +54,11 @@ + + + + + @@ -66,6 +71,14 @@ + + + + + + + +