1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-03 17:13:50 +00:00

add shared controllers and view to ios core

This commit is contained in:
Kyle Spearrin
2019-06-27 15:48:25 -04:00
parent fb3009fc66
commit 9c2cbc0ecb
15 changed files with 1546 additions and 0 deletions

View File

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

View File

@@ -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<CipherViewModel> _allItems = new List<CipherViewModel>();
protected ICipherService _cipherService;
protected ITotpService _totpService;
protected IUserService _userService;
private AppExtensionContext _context;
private UIViewController _controller;
public ExtensionTableSource(AppExtensionContext context, UIViewController controller)
{
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_userService = ServiceContainer.Resolve<IUserService>("userService");
_context = context;
_controller = controller;
}
public IEnumerable<CipherViewModel> Items { get; private set; }
public async Task LoadItemsAsync(bool urlFilter = true, string searchFilter = null)
{
var combinedLogins = new List<CipherView>();
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<CipherViewModel>();
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<CipherViewModel> 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<string> 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;
}
}
}

View File

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

View File

@@ -0,0 +1,7 @@
namespace Bit.iOS.Core.Views
{
public interface ISelectable
{
void Select();
}
}

View File

@@ -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<string> _items = new List<string>();
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<string> 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);
}
}
}
}

View File

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

View File

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

View File

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