mirror of
https://github.com/bitwarden/android.git
synced 2024-12-25 18:38:27 +03:00
Send feature for mobile (#1256)
* Send feature for mobile * added fallback for KdfIterations * additional property exclusions for tests * support encryptedFileData as byte array comparison in SendServiceTests * formatting * requested changes * additional changes * change position of send service registration to match declaration order
This commit is contained in:
parent
52ba9f2ba7
commit
a18e59a28a
49 changed files with 3776 additions and 33 deletions
|
@ -123,7 +123,9 @@
|
||||||
<Compile Include="Receivers\LockAlarmReceiver.cs" />
|
<Compile Include="Receivers\LockAlarmReceiver.cs" />
|
||||||
<Compile Include="Receivers\PackageReplacedReceiver.cs" />
|
<Compile Include="Receivers\PackageReplacedReceiver.cs" />
|
||||||
<Compile Include="Renderers\CipherViewCellRenderer.cs" />
|
<Compile Include="Renderers\CipherViewCellRenderer.cs" />
|
||||||
|
<Compile Include="Renderers\ExtendedDatePickerRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomTabbedRenderer.cs" />
|
<Compile Include="Renderers\CustomTabbedRenderer.cs" />
|
||||||
|
<Compile Include="Renderers\ExtendedTimePickerRenderer.cs" />
|
||||||
<Compile Include="Renderers\ExtendedSliderRenderer.cs" />
|
<Compile Include="Renderers\ExtendedSliderRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomEditorRenderer.cs" />
|
<Compile Include="Renderers\CustomEditorRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomPickerRenderer.cs" />
|
<Compile Include="Renderers\CustomPickerRenderer.cs" />
|
||||||
|
@ -131,6 +133,7 @@
|
||||||
<Compile Include="Renderers\CustomSearchBarRenderer.cs" />
|
<Compile Include="Renderers\CustomSearchBarRenderer.cs" />
|
||||||
<Compile Include="Renderers\ExtendedListViewRenderer.cs" />
|
<Compile Include="Renderers\ExtendedListViewRenderer.cs" />
|
||||||
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
|
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
|
||||||
|
<Compile Include="Renderers\SendViewCellRenderer.cs" />
|
||||||
<Compile Include="Services\AndroidPushNotificationService.cs" />
|
<Compile Include="Services\AndroidPushNotificationService.cs" />
|
||||||
<Compile Include="Services\AndroidLogService.cs" />
|
<Compile Include="Services\AndroidLogService.cs" />
|
||||||
<Compile Include="MainApplication.cs" />
|
<Compile Include="MainApplication.cs" />
|
||||||
|
@ -176,6 +179,7 @@
|
||||||
<AndroidResource Include="Resources\drawable\login.xml" />
|
<AndroidResource Include="Resources\drawable\login.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\logo.xml" />
|
<AndroidResource Include="Resources\drawable\logo.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\logo_white.xml" />
|
<AndroidResource Include="Resources\drawable\logo_white.xml" />
|
||||||
|
<AndroidResource Include="Resources\drawable\paper_plane.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\pencil.xml" />
|
<AndroidResource Include="Resources\drawable\pencil.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\plus.xml" />
|
<AndroidResource Include="Resources\drawable\plus.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\refresh.xml" />
|
<AndroidResource Include="Resources\drawable\refresh.xml" />
|
||||||
|
@ -183,6 +187,7 @@
|
||||||
<AndroidResource Include="Resources\drawable\shield.xml" />
|
<AndroidResource Include="Resources\drawable\shield.xml" />
|
||||||
<AndroidResource Include="Resources\drawable-v23\splash_screen.xml" />
|
<AndroidResource Include="Resources\drawable-v23\splash_screen.xml" />
|
||||||
<AndroidResource Include="Resources\drawable-v23\splash_screen_dark.xml" />
|
<AndroidResource Include="Resources\drawable-v23\splash_screen_dark.xml" />
|
||||||
|
<AndroidResource Include="Resources\layout\SendViewCell.axml" />
|
||||||
<AndroidResource Include="Resources\layout\Tabbar.axml" />
|
<AndroidResource Include="Resources\layout\Tabbar.axml" />
|
||||||
<AndroidResource Include="Resources\layout\Toolbar.axml" />
|
<AndroidResource Include="Resources\layout\Toolbar.axml" />
|
||||||
<AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher.xml" />
|
<AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher.xml" />
|
||||||
|
|
50
src/Android/Renderers/ExtendedDatePickerRenderer.cs
Normal file
50
src/Android/Renderers/ExtendedDatePickerRenderer.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Views;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.Droid.Renderers;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(ExtendedDatePicker), typeof(ExtendedDatePickerRenderer))]
|
||||||
|
namespace Bit.Droid.Renderers
|
||||||
|
{
|
||||||
|
public class ExtendedDatePickerRenderer : DatePickerRenderer
|
||||||
|
{
|
||||||
|
public ExtendedDatePickerRenderer(Context context)
|
||||||
|
: base(context) { }
|
||||||
|
|
||||||
|
protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e)
|
||||||
|
{
|
||||||
|
base.OnElementChanged(e);
|
||||||
|
if (Control != null && Element is ExtendedDatePicker element)
|
||||||
|
{
|
||||||
|
// center text
|
||||||
|
Control.Gravity = GravityFlags.CenterHorizontal;
|
||||||
|
|
||||||
|
// use placeholder until NullableDate set
|
||||||
|
if (!element.NullableDate.HasValue)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == DatePicker.DateProperty.PropertyName ||
|
||||||
|
e.PropertyName == DatePicker.FormatProperty.PropertyName)
|
||||||
|
{
|
||||||
|
if (Control != null && Element is ExtendedDatePicker element)
|
||||||
|
{
|
||||||
|
if (Element.Format == element.PlaceHolder)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnElementPropertyChanged(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
src/Android/Renderers/ExtendedTimePickerRenderer.cs
Normal file
50
src/Android/Renderers/ExtendedTimePickerRenderer.cs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Views;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.Droid.Renderers;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(ExtendedTimePicker), typeof(ExtendedTimePickerRenderer))]
|
||||||
|
namespace Bit.Droid.Renderers
|
||||||
|
{
|
||||||
|
public class ExtendedTimePickerRenderer : TimePickerRenderer
|
||||||
|
{
|
||||||
|
public ExtendedTimePickerRenderer(Context context)
|
||||||
|
: base(context) { }
|
||||||
|
|
||||||
|
protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e)
|
||||||
|
{
|
||||||
|
base.OnElementChanged(e);
|
||||||
|
if (Control != null && Element is ExtendedTimePicker element)
|
||||||
|
{
|
||||||
|
// center text
|
||||||
|
Control.Gravity = GravityFlags.CenterHorizontal;
|
||||||
|
|
||||||
|
// use placeholder until NullableTime set
|
||||||
|
if (!element.NullableTime.HasValue)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == TimePicker.TimeProperty.PropertyName ||
|
||||||
|
e.PropertyName == TimePicker.FormatProperty.PropertyName)
|
||||||
|
{
|
||||||
|
if (Control != null && Element is ExtendedTimePicker element)
|
||||||
|
{
|
||||||
|
if (Element.Format == element.PlaceHolder)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnElementPropertyChanged(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
199
src/Android/Renderers/SendViewCellRenderer.cs
Normal file
199
src/Android/Renderers/SendViewCellRenderer.cs
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Graphics;
|
||||||
|
using Android.Util;
|
||||||
|
using Android.Views;
|
||||||
|
using Android.Widget;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Droid.Renderers;
|
||||||
|
using FFImageLoading.Work;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
using Button = Android.Widget.Button;
|
||||||
|
using Color = Android.Graphics.Color;
|
||||||
|
using View = Android.Views.View;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(SendViewCell), typeof(SendViewCellRenderer))]
|
||||||
|
namespace Bit.Droid.Renderers
|
||||||
|
{
|
||||||
|
public class SendViewCellRenderer : ViewCellRenderer
|
||||||
|
{
|
||||||
|
private static Typeface _faTypeface;
|
||||||
|
private static Typeface _miTypeface;
|
||||||
|
private static Color _textColor;
|
||||||
|
private static Color _mutedColor;
|
||||||
|
private static Color _disabledIconColor;
|
||||||
|
private static bool _usingLightTheme;
|
||||||
|
|
||||||
|
private AndroidSendCell _cell;
|
||||||
|
|
||||||
|
protected override View GetCellCore(Cell item, View convertView,
|
||||||
|
ViewGroup parent, Context context)
|
||||||
|
{
|
||||||
|
// TODO expand beyond light/dark detection once we support custom theme switching without app restart
|
||||||
|
var themeChanged = _usingLightTheme != ThemeManager.UsingLightTheme;
|
||||||
|
if (_faTypeface == null)
|
||||||
|
{
|
||||||
|
_faTypeface = Typeface.CreateFromAsset(context.Assets, "FontAwesome.ttf");
|
||||||
|
}
|
||||||
|
if (_miTypeface == null)
|
||||||
|
{
|
||||||
|
_miTypeface = Typeface.CreateFromAsset(context.Assets, "MaterialIcons_Regular.ttf");
|
||||||
|
}
|
||||||
|
if (_textColor == default(Color) || themeChanged)
|
||||||
|
{
|
||||||
|
_textColor = ThemeManager.GetResourceColor("TextColor").ToAndroid();
|
||||||
|
}
|
||||||
|
if (_mutedColor == default(Color) || themeChanged)
|
||||||
|
{
|
||||||
|
_mutedColor = ThemeManager.GetResourceColor("MutedColor").ToAndroid();
|
||||||
|
}
|
||||||
|
if (_disabledIconColor == default(Color) || themeChanged)
|
||||||
|
{
|
||||||
|
_disabledIconColor = ThemeManager.GetResourceColor("DisabledIconColor").ToAndroid();
|
||||||
|
}
|
||||||
|
_usingLightTheme = ThemeManager.UsingLightTheme;
|
||||||
|
|
||||||
|
var sendCell = item as SendViewCell;
|
||||||
|
_cell = convertView as AndroidSendCell;
|
||||||
|
if (_cell == null)
|
||||||
|
{
|
||||||
|
_cell = new AndroidSendCell(context, sendCell, _faTypeface, _miTypeface);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cell.SendViewCell.PropertyChanged -= CellPropertyChanged;
|
||||||
|
}
|
||||||
|
sendCell.PropertyChanged += CellPropertyChanged;
|
||||||
|
_cell.UpdateCell(sendCell);
|
||||||
|
_cell.UpdateColors(_textColor, _mutedColor, _disabledIconColor);
|
||||||
|
return _cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CellPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var sendCell = sender as SendViewCell;
|
||||||
|
_cell.SendViewCell = sendCell;
|
||||||
|
if (e.PropertyName == SendViewCell.SendProperty.PropertyName)
|
||||||
|
{
|
||||||
|
_cell.UpdateCell(sendCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AndroidSendCell : LinearLayout, INativeElementView
|
||||||
|
{
|
||||||
|
private readonly Typeface _faTypeface;
|
||||||
|
private readonly Typeface _miTypeface;
|
||||||
|
|
||||||
|
private IScheduledWork _currentTask;
|
||||||
|
|
||||||
|
public AndroidSendCell(Context context, SendViewCell sendView, Typeface faTypeface, Typeface miTypeface)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
SendViewCell = sendView;
|
||||||
|
_faTypeface = faTypeface;
|
||||||
|
_miTypeface = miTypeface;
|
||||||
|
|
||||||
|
var view = (context as Activity).LayoutInflater.Inflate(Resource.Layout.SendViewCell, null);
|
||||||
|
Icon = view.FindViewById<TextView>(Resource.Id.SendCellIcon);
|
||||||
|
Name = view.FindViewById<TextView>(Resource.Id.SendCellName);
|
||||||
|
SubTitle = view.FindViewById<TextView>(Resource.Id.SendCellSubTitle);
|
||||||
|
HasPasswordIcon = view.FindViewById<TextView>(Resource.Id.SendCellHasPasswordIcon);
|
||||||
|
MaxAccessCountReachedIcon = view.FindViewById<TextView>(Resource.Id.SendCellMaxAccessCountReachedIcon);
|
||||||
|
ExpiredIcon = view.FindViewById<TextView>(Resource.Id.SendCellExpiredIcon);
|
||||||
|
PendingDeleteIcon = view.FindViewById<TextView>(Resource.Id.SendCellPendingDeleteIcon);
|
||||||
|
MoreButton = view.FindViewById<Button>(Resource.Id.SendCellButton);
|
||||||
|
MoreButton.Click += MoreButton_Click;
|
||||||
|
|
||||||
|
Icon.Typeface = _faTypeface;
|
||||||
|
HasPasswordIcon.Typeface = _faTypeface;
|
||||||
|
MaxAccessCountReachedIcon.Typeface = _faTypeface;
|
||||||
|
ExpiredIcon.Typeface = _faTypeface;
|
||||||
|
PendingDeleteIcon.Typeface = _faTypeface;
|
||||||
|
MoreButton.Typeface = _miTypeface;
|
||||||
|
|
||||||
|
var small = (float)Device.GetNamedSize(NamedSize.Small, typeof(Label));
|
||||||
|
Icon.SetTextSize(ComplexUnitType.Pt, 10);
|
||||||
|
Name.SetTextSize(ComplexUnitType.Sp, (float)Device.GetNamedSize(NamedSize.Medium, typeof(Label)));
|
||||||
|
SubTitle.SetTextSize(ComplexUnitType.Sp, small);
|
||||||
|
HasPasswordIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||||
|
MaxAccessCountReachedIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||||
|
ExpiredIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||||
|
PendingDeleteIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||||
|
MoreButton.SetTextSize(ComplexUnitType.Sp, 25);
|
||||||
|
|
||||||
|
AddView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendViewCell SendViewCell { get; set; }
|
||||||
|
public Element Element => SendViewCell;
|
||||||
|
|
||||||
|
public TextView Icon { get; set; }
|
||||||
|
public TextView Name { get; set; }
|
||||||
|
public TextView SubTitle { get; set; }
|
||||||
|
public TextView HasPasswordIcon { get; set; }
|
||||||
|
public TextView MaxAccessCountReachedIcon { get; set; }
|
||||||
|
public TextView ExpiredIcon { get; set; }
|
||||||
|
public TextView PendingDeleteIcon { get; set; }
|
||||||
|
public Button MoreButton { get; set; }
|
||||||
|
|
||||||
|
public void UpdateCell(SendViewCell sendCell)
|
||||||
|
{
|
||||||
|
UpdateIconImage(sendCell);
|
||||||
|
|
||||||
|
var send = sendCell.Send;
|
||||||
|
Name.Text = send.Name;
|
||||||
|
SubTitle.Text = send.DisplayDate;
|
||||||
|
HasPasswordIcon.Visibility = send.HasPassword ? ViewStates.Visible : ViewStates.Gone;
|
||||||
|
MaxAccessCountReachedIcon.Visibility = send.MaxAccessCountReached ? ViewStates.Visible : ViewStates.Gone;
|
||||||
|
ExpiredIcon.Visibility = send.Expired ? ViewStates.Visible : ViewStates.Gone;
|
||||||
|
PendingDeleteIcon.Visibility = send.PendingDelete ? ViewStates.Visible : ViewStates.Gone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateIconImage(SendViewCell sendCell)
|
||||||
|
{
|
||||||
|
if (_currentTask != null && !_currentTask.IsCancelled && !_currentTask.IsCompleted)
|
||||||
|
{
|
||||||
|
_currentTask.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = sendCell.Send;
|
||||||
|
var iconImage = sendCell.GetIconImage(send);
|
||||||
|
Icon.Text = iconImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateColors(Color textColor, Color mutedColor,
|
||||||
|
Color iconDisabledColor)
|
||||||
|
{
|
||||||
|
Name.SetTextColor(textColor);
|
||||||
|
SubTitle.SetTextColor(mutedColor);
|
||||||
|
Icon.SetTextColor(mutedColor);
|
||||||
|
HasPasswordIcon.SetTextColor(mutedColor);
|
||||||
|
MaxAccessCountReachedIcon.SetTextColor(mutedColor);
|
||||||
|
ExpiredIcon.SetTextColor(mutedColor);
|
||||||
|
PendingDeleteIcon.SetTextColor(mutedColor);
|
||||||
|
MoreButton.SetTextColor(iconDisabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoreButton_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (SendViewCell.ButtonCommand?.CanExecute(SendViewCell.Send) ?? false)
|
||||||
|
{
|
||||||
|
SendViewCell.ButtonCommand.Execute(SendViewCell.Send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
MoreButton.Click -= MoreButton_Click;
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/Android/Resources/drawable/paper_plane.xml
Normal file
9
src/Android/Resources/drawable/paper_plane.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="600"
|
||||||
|
android:viewportHeight="600">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M508.757,52.805L68.978,306.52C51.804,316.388 53.987,340.299 71.065,347.51L171.925,389.827L444.522,149.585C449.741,144.936 457.141,152.052 452.682,157.46L224.111,435.94L224.111,512.32C224.111,534.712 251.152,543.536 264.436,527.312L324.686,453.967L442.909,503.496C456.382,509.189 471.753,500.744 474.22,486.227L542.536,76.336C545.762,57.17 525.172,43.317 508.757,52.805Z" />
|
||||||
|
</vector>
|
96
src/Android/Resources/layout/SendViewCell.axml
Normal file
96
src/Android/Resources/layout/SendViewCell.axml
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="44dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingLeft="2.2dp">
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="39.8dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellIcon"
|
||||||
|
android:layout_width="26dp"
|
||||||
|
android:layout_height="26dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="[X]" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/SendCellContent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingVertical="7.65dp">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/SendCellContentTop"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:text="Name" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellHasPasswordIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellMaxAccessCountReachedIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellExpiredIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellPendingDeleteIcon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingLeft="5dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="" />
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/SendCellSubTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:text="SubTitle" />
|
||||||
|
</LinearLayout>
|
||||||
|
<Button
|
||||||
|
android:id="@+id/SendCellButton"
|
||||||
|
android:layout_width="37dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:text=""
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:background="@android:color/transparent" />
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
|
@ -24,7 +24,6 @@
|
||||||
<item name="colorControlNormal">@color/border</item>
|
<item name="colorControlNormal">@color/border</item>
|
||||||
<item name="android:navigationBarColor">@android:color/black</item>
|
<item name="android:navigationBarColor">@android:color/black</item>
|
||||||
<item name="windowActionModeOverlay">true</item>
|
<item name="windowActionModeOverlay">true</item>
|
||||||
<item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
|
|
||||||
<item name="android:colorActivatedHighlight">@android:color/transparent</item>
|
<item name="android:colorActivatedHighlight">@android:color/transparent</item>
|
||||||
<item name="android:textCursorDrawable">@null</item>
|
<item name="android:textCursorDrawable">@null</item>
|
||||||
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
|
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
|
||||||
|
@ -47,7 +46,6 @@
|
||||||
<item name="colorControlNormal">@color/dark_border</item>
|
<item name="colorControlNormal">@color/dark_border</item>
|
||||||
<item name="android:navigationBarColor">@color/dark_navigationBarBackground</item>
|
<item name="android:navigationBarColor">@color/dark_navigationBarBackground</item>
|
||||||
<item name="windowActionModeOverlay">true</item>
|
<item name="windowActionModeOverlay">true</item>
|
||||||
<item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
|
|
||||||
<item name="android:colorActivatedHighlight">@android:color/transparent</item>
|
<item name="android:colorActivatedHighlight">@android:color/transparent</item>
|
||||||
<item name="android:textCursorDrawable">@null</item>
|
<item name="android:textCursorDrawable">@null</item>
|
||||||
<item name="popupTheme">@style/ThemeOverlay.AppCompat</item>
|
<item name="popupTheme">@style/ThemeOverlay.AppCompat</item>
|
||||||
|
@ -95,9 +93,4 @@
|
||||||
<item name="android:colorBackground">@color/nord_popupBackground</item>
|
<item name="android:colorBackground">@color/nord_popupBackground</item>
|
||||||
<item name="android:textColor">@color/nord_popupText</item>
|
<item name="android:textColor">@color/nord_popupText</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Other theme components -->
|
|
||||||
<style name="AppCompatDialogStyle" parent="Theme.AppCompat.Light.Dialog">
|
|
||||||
<item name="colorAccent">#FF4081</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -116,6 +116,10 @@
|
||||||
<DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon>
|
<DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="Pages\Send\SendGroupingsPage\SendGroupingsPage.xaml.cs">
|
||||||
|
<DependentUpon>SendGroupingsPage.xaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
86
src/App/Controls/ExtendedDatePicker.cs
Normal file
86
src/App/Controls/ExtendedDatePicker.cs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class ExtendedDatePicker : DatePicker
|
||||||
|
{
|
||||||
|
private string _format;
|
||||||
|
|
||||||
|
public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create(
|
||||||
|
nameof(PlaceHolder), typeof(string), typeof(ExtendedDatePicker));
|
||||||
|
|
||||||
|
public string PlaceHolder
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(PlaceHolderProperty); }
|
||||||
|
set { SetValue(PlaceHolderProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly BindableProperty NullableDateProperty = BindableProperty.Create(
|
||||||
|
nameof(NullableDate), typeof(DateTime?), typeof(ExtendedDatePicker));
|
||||||
|
|
||||||
|
public DateTime? NullableDate
|
||||||
|
{
|
||||||
|
get { return (DateTime?)GetValue(NullableDateProperty); }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetValue(NullableDateProperty, value);
|
||||||
|
UpdateDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDate()
|
||||||
|
{
|
||||||
|
if (NullableDate.HasValue)
|
||||||
|
{
|
||||||
|
if (_format != null)
|
||||||
|
{
|
||||||
|
Format = _format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Format = PlaceHolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnBindingContextChanged()
|
||||||
|
{
|
||||||
|
base.OnBindingContextChanged();
|
||||||
|
if (BindingContext != null)
|
||||||
|
{
|
||||||
|
_format = Format;
|
||||||
|
UpdateDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
|
||||||
|
if (propertyName == DateProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName &&
|
||||||
|
!IsFocused && (Date.ToString("d") ==
|
||||||
|
DateTime.Now.ToString("d"))))
|
||||||
|
{
|
||||||
|
NullableDate = Date;
|
||||||
|
UpdateDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyName == NullableDateProperty.PropertyName)
|
||||||
|
{
|
||||||
|
if (NullableDate.HasValue)
|
||||||
|
{
|
||||||
|
Date = NullableDate.Value;
|
||||||
|
if (Date.ToString(_format) == DateTime.Now.ToString(_format))
|
||||||
|
{
|
||||||
|
UpdateDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
src/App/Controls/ExtendedTimePicker.cs
Normal file
86
src/App/Controls/ExtendedTimePicker.cs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class ExtendedTimePicker : TimePicker
|
||||||
|
{
|
||||||
|
private string _format;
|
||||||
|
|
||||||
|
public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create(
|
||||||
|
nameof(PlaceHolder), typeof(string), typeof(ExtendedTimePicker));
|
||||||
|
|
||||||
|
public string PlaceHolder
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(PlaceHolderProperty); }
|
||||||
|
set { SetValue(PlaceHolderProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly BindableProperty NullableTimeProperty = BindableProperty.Create(
|
||||||
|
nameof(NullableTime), typeof(TimeSpan?), typeof(ExtendedTimePicker));
|
||||||
|
|
||||||
|
public TimeSpan? NullableTime
|
||||||
|
{
|
||||||
|
get { return (TimeSpan?)GetValue(NullableTimeProperty); }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetValue(NullableTimeProperty, value);
|
||||||
|
UpdateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTime()
|
||||||
|
{
|
||||||
|
if (NullableTime.HasValue)
|
||||||
|
{
|
||||||
|
if (_format != null)
|
||||||
|
{
|
||||||
|
Format = _format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Format = PlaceHolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnBindingContextChanged()
|
||||||
|
{
|
||||||
|
base.OnBindingContextChanged();
|
||||||
|
if (BindingContext != null)
|
||||||
|
{
|
||||||
|
_format = Format;
|
||||||
|
UpdateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
|
||||||
|
if (propertyName == TimeProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName &&
|
||||||
|
!IsFocused && (Time.ToString("t") ==
|
||||||
|
DateTime.Now.TimeOfDay.ToString("t"))))
|
||||||
|
{
|
||||||
|
NullableTime = Time;
|
||||||
|
UpdateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyName == NullableTimeProperty.PropertyName)
|
||||||
|
{
|
||||||
|
if (NullableTime.HasValue)
|
||||||
|
{
|
||||||
|
Time = NullableTime.Value;
|
||||||
|
if (Time.ToString(_format) == DateTime.Now.TimeOfDay.ToString(_format))
|
||||||
|
{
|
||||||
|
UpdateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
src/App/Controls/SendViewCell/SendViewCell.xaml
Normal file
124
src/App/Controls/SendViewCell/SendViewCell.xaml
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Controls.SendViewCell"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities">
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
x:Name="_grid"
|
||||||
|
StyleClass="list-row, list-row-platform"
|
||||||
|
RowSpacing="0"
|
||||||
|
ColumnSpacing="0"
|
||||||
|
x:DataType="controls:SendViewCellViewModel">
|
||||||
|
|
||||||
|
<Grid.BindingContext>
|
||||||
|
<controls:SendViewCellViewModel />
|
||||||
|
</Grid.BindingContext>
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="40" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="60" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<controls:FaLabel
|
||||||
|
x:Name="_icon"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-icon, list-icon-platform"
|
||||||
|
AutomationProperties.IsInAccessibleTree="False" />
|
||||||
|
|
||||||
|
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
StyleClass="list-title, list-title-platform"
|
||||||
|
Text="{Binding Send.Name, Mode=OneWay}" />
|
||||||
|
<Label
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.ColumnSpan="5"
|
||||||
|
StyleClass="list-subtitle, list-subtitle-platform"
|
||||||
|
Text="{Binding Send.DisplayDate, Mode=OneWay}" />
|
||||||
|
<controls:FaLabel
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-title-icon"
|
||||||
|
Margin="5, 0, 0, 0"
|
||||||
|
Text=""
|
||||||
|
IsVisible="{Binding Send.HasPassword, Mode=OneWay}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Password}" />
|
||||||
|
<controls:FaLabel
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-title-icon"
|
||||||
|
Margin="5, 0, 0, 0"
|
||||||
|
Text=""
|
||||||
|
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneWay}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
|
||||||
|
<controls:FaLabel
|
||||||
|
Grid.Column="3"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-title-icon"
|
||||||
|
Margin="5, 0, 0, 0"
|
||||||
|
Text=""
|
||||||
|
IsVisible="{Binding Send.Expired, Mode=OneWay}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Expired}" />
|
||||||
|
<controls:FaLabel
|
||||||
|
Grid.Column="4"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-title-icon"
|
||||||
|
Margin="5, 0, 0, 0"
|
||||||
|
Text=""
|
||||||
|
IsVisible="{Binding Send.PendingDelete, Mode=OneWay}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n PendingDelete}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<controls:MiButton
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Text=""
|
||||||
|
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||||
|
Clicked="MoreButton_Clicked"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
HorizontalOptions="EndAndExpand"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</ViewCell>
|
110
src/App/Controls/SendViewCell/SendViewCell.xaml.cs
Normal file
110
src/App/Controls/SendViewCell/SendViewCell.xaml.cs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
using System;
|
||||||
|
using Bit.App.Pages;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class SendViewCell : ViewCell
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty SendProperty = BindableProperty.Create(
|
||||||
|
nameof(Send), typeof(SendView), typeof(SendViewCell), default(SendView), BindingMode.OneWay);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(ButtonCommand), typeof(Command<SendView>), typeof(SendViewCell));
|
||||||
|
|
||||||
|
private readonly IEnvironmentService _environmentService;
|
||||||
|
|
||||||
|
private SendViewCellViewModel _viewModel;
|
||||||
|
private bool _usingNativeCell;
|
||||||
|
|
||||||
|
public SendViewCell()
|
||||||
|
{
|
||||||
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_viewModel = _grid.BindingContext as SendViewCellViewModel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_usingNativeCell = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendView Send
|
||||||
|
{
|
||||||
|
get => GetValue(SendProperty) as SendView;
|
||||||
|
set => SetValue(SendProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Command<SendView> ButtonCommand
|
||||||
|
{
|
||||||
|
get => GetValue(ButtonCommandProperty) as Command<SendView>;
|
||||||
|
set => SetValue(ButtonCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
if (_usingNativeCell)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (propertyName == SendProperty.PropertyName)
|
||||||
|
{
|
||||||
|
_viewModel.Send = Send;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnBindingContextChanged()
|
||||||
|
{
|
||||||
|
base.OnBindingContextChanged();
|
||||||
|
if (_usingNativeCell)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendView send = null;
|
||||||
|
if (BindingContext is SendGroupingsPageListItem sendGroupingsPageListItem)
|
||||||
|
{
|
||||||
|
send = sendGroupingsPageListItem.Send;
|
||||||
|
}
|
||||||
|
else if (BindingContext is SendView sv)
|
||||||
|
{
|
||||||
|
send = sv;
|
||||||
|
}
|
||||||
|
if (send != null)
|
||||||
|
{
|
||||||
|
var iconImage = GetIconImage(send);
|
||||||
|
_icon.IsVisible = true;
|
||||||
|
_icon.Text = iconImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetIconImage(SendView send)
|
||||||
|
{
|
||||||
|
string icon = null;
|
||||||
|
switch (send.Type)
|
||||||
|
{
|
||||||
|
case SendType.Text:
|
||||||
|
icon = "\uf0f6"; // fa-file-text-o
|
||||||
|
break;
|
||||||
|
case SendType.File:
|
||||||
|
icon = "\uf016"; // fa-file-o
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoreButton_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ButtonCommand?.Execute(Send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/App/Controls/SendViewCell/SendViewCellViewModel.cs
Normal file
16
src/App/Controls/SendViewCell/SendViewCellViewModel.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class SendViewCellViewModel : ExtendedViewModel
|
||||||
|
{
|
||||||
|
private SendView _send;
|
||||||
|
|
||||||
|
public SendView Send
|
||||||
|
{
|
||||||
|
get => _send;
|
||||||
|
set => SetProperty(ref _send, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
{
|
{
|
||||||
public string Page { get; set; }
|
public string Page { get; set; }
|
||||||
public string CipherId { get; set; }
|
public string CipherId { get; set; }
|
||||||
|
public string SendId { get; set; }
|
||||||
public string SearchText { get; set; }
|
public string SearchText { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
340
src/App/Pages/Send/SendAddEditPage.xaml
Normal file
340
src/App/Pages/Send/SendAddEditPage.xaml
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.SendAddEditPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:DataType="pages:SendAddEditPageViewModel"
|
||||||
|
x:Name="_page"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:SendAddEditPageViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.ToolbarItems>
|
||||||
|
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" />
|
||||||
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
<u:IsNullConverter x:Key="null" />
|
||||||
|
<u:IsNotNullConverter x:Key="notNull" />
|
||||||
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
|
x:Key="closeItem" x:Name="_closeItem" />
|
||||||
|
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
|
||||||
|
x:Key="moreItem"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
|
<ToolbarItem Text="{u:I18n RemovePassword}"
|
||||||
|
Clicked="RemovePassword_Clicked"
|
||||||
|
Order="Secondary"
|
||||||
|
IsDestructive="True"
|
||||||
|
x:Name="_removePassword"
|
||||||
|
x:Key="removePassword" />
|
||||||
|
<ToolbarItem Text="{u:I18n CopyLink}"
|
||||||
|
Clicked="CopyLink_Clicked"
|
||||||
|
Order="Secondary"
|
||||||
|
IsDestructive="True"
|
||||||
|
x:Name="_copyLink"
|
||||||
|
x:Key="copyLink" />
|
||||||
|
<ToolbarItem Text="{u:I18n ShareLink}"
|
||||||
|
Clicked="ShareLink_Clicked"
|
||||||
|
Order="Secondary"
|
||||||
|
IsDestructive="True"
|
||||||
|
x:Name="_shareLink"
|
||||||
|
x:Key="shareLink" />
|
||||||
|
<ToolbarItem Text="{u:I18n Delete}"
|
||||||
|
Clicked="Delete_Clicked"
|
||||||
|
Order="Secondary"
|
||||||
|
IsDestructive="True"
|
||||||
|
x:Name="_deleteItem"
|
||||||
|
x:Key="deleteItem" />
|
||||||
|
|
||||||
|
<ScrollView x:Key="scrollView" x:Name="_scrollView">
|
||||||
|
<StackLayout Spacing="20">
|
||||||
|
<StackLayout StyleClass="box">
|
||||||
|
<StackLayout StyleClass="box-row"
|
||||||
|
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Type}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Picker
|
||||||
|
x:Name="_typePicker"
|
||||||
|
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Name}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Entry
|
||||||
|
x:Name="_nameEntry"
|
||||||
|
Text="{Binding Send.Name}"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row"
|
||||||
|
IsVisible="{Binding IsText}">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n TypeText}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Editor
|
||||||
|
x:Name="_textEditor"
|
||||||
|
AutoSize="TextChanges"
|
||||||
|
Text="{Binding Send.Text.Text}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Margin="{Binding EditorMargins}" />
|
||||||
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowEditorSeparators}" />
|
||||||
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n HideTextByDefault}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding Send.Text.Hidden}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row"
|
||||||
|
IsVisible="{Binding IsFile}">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n TypeFile}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Label
|
||||||
|
Text="{Binding Send.File.FileName, Mode=OneWay}"
|
||||||
|
IsVisible="{Binding EditMode}"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
<StackLayout
|
||||||
|
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||||
|
StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
IsVisible="{Binding FileName, Converter={StaticResource null}}"
|
||||||
|
Text="{u:I18n NoFileChosen}"
|
||||||
|
LineBreakMode="CharacterWrap"
|
||||||
|
StyleClass="text-sm, text-muted"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<Label
|
||||||
|
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
|
||||||
|
Text="{Binding FileName}"
|
||||||
|
LineBreakMode="CharacterWrap"
|
||||||
|
StyleClass="text-sm, text-muted"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<Button Text="{u:I18n ChooseFile}"
|
||||||
|
StyleClass="box-button-row"
|
||||||
|
Clicked="ChooseFile_Clicked" />
|
||||||
|
<Label
|
||||||
|
Margin="0, 10, 0, 0"
|
||||||
|
Text="{u:I18n MaxFileSize}"
|
||||||
|
StyleClass="box-footer-label" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row-header">
|
||||||
|
<Label Text="{u:I18n Options, Header=True}"
|
||||||
|
StyleClass="box-header, box-header-platform" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DeletionDate}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Picker
|
||||||
|
x:Name="_deletionDateTypePicker"
|
||||||
|
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||||
|
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n DeletionTime}" />
|
||||||
|
<Grid
|
||||||
|
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<controls:ExtendedDatePicker
|
||||||
|
Date="{Binding DeletionDate}"
|
||||||
|
Format="d"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n DeletionDate}"
|
||||||
|
Grid.Column="0" />
|
||||||
|
<controls:ExtendedTimePicker
|
||||||
|
Time="{Binding DeletionTime}"
|
||||||
|
Format="t"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n DeletionTime}"
|
||||||
|
Grid.Column="1" />
|
||||||
|
</Grid>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DeletionDateInfo}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ExpirationDate}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Picker
|
||||||
|
x:Name="_expirationDateTypePicker"
|
||||||
|
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||||
|
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ExpirationTime}" />
|
||||||
|
<Grid
|
||||||
|
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<controls:ExtendedDatePicker
|
||||||
|
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
|
||||||
|
PlaceHolder="mm/dd/yyyy"
|
||||||
|
Format="d"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
||||||
|
Grid.Column="0" />
|
||||||
|
<controls:ExtendedTimePicker
|
||||||
|
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
|
||||||
|
PlaceHolder="--:-- --"
|
||||||
|
Format="t"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ExpirationTime}"
|
||||||
|
Grid.Column="1" />
|
||||||
|
</Grid>
|
||||||
|
<StackLayout Orientation="Horizontal" Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ExpirationDateInfo}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
HorizontalOptions="StartAndExpand"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n Clear}"
|
||||||
|
IsVisible="{Binding EditMode}"
|
||||||
|
WidthRequest="110"
|
||||||
|
StyleClass="box-row-button"
|
||||||
|
Clicked="ClearExpirationDate_Clicked" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView StyleClass="box-row-separator"
|
||||||
|
Margin="0, 5, 0, 0" />
|
||||||
|
<StackLayout StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n MaximumAccessCount}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<StackLayout StyleClass="box-row" Orientation="Horizontal">
|
||||||
|
<Entry
|
||||||
|
Text="{Binding MaxAccessCount}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Keyboard="Numeric"
|
||||||
|
MaxLength="9"
|
||||||
|
TextChanged="OnMaxAccessCountTextChanged"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
<Stepper
|
||||||
|
x:Name="_maxAccessCountStepper"
|
||||||
|
Value="{Binding MaxAccessCount}"
|
||||||
|
Maximum="999999999"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n MaximumAccessCountInfo}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Margin="0,5" />
|
||||||
|
<StackLayout
|
||||||
|
IsVisible="{Binding EditMode}"
|
||||||
|
StyleClass="box-row"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n CurrentAccessCount}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
HorizontalOptions="StartAndExpand"
|
||||||
|
VerticalTextAlignment="Center" />
|
||||||
|
<Label
|
||||||
|
Text="{Binding Send.AccessCount, Mode=OneWay}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
HorizontalOptions="End" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView StyleClass="box-row-separator" />
|
||||||
|
<StackLayout StyleClass="box-row" Margin="0,5">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NewPassword}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<StackLayout Orientation="Horizontal">
|
||||||
|
<Entry
|
||||||
|
Text="{Binding NewPassword}"
|
||||||
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
IsSpellCheckEnabled="False"
|
||||||
|
IsTextPredictionEnabled="False"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
<controls:FaButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding ShowPasswordIcon}"
|
||||||
|
Command="{Binding TogglePasswordCommand}"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n PasswordInfo}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView StyleClass="box-row-separator" />
|
||||||
|
<StackLayout StyleClass="box-row" Margin="0,5">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Notes}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Editor
|
||||||
|
AutoSize="TextChanges"
|
||||||
|
Text="{Binding Send.Notes}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Margin="{Binding EditorMargins}" />
|
||||||
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowEditorSeparators}" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NotesInfo}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView StyleClass="box-row-separator" />
|
||||||
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DisableSend}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding Send.Disabled}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ShareOnSave}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding ShareOnSave}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView StyleClass="box-row-separator" />
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
|
||||||
|
</pages:BaseContentPage>
|
228
src/App/Pages/Send/SendAddEditPage.xaml.cs
Normal file
228
src/App/Pages/Send/SendAddEditPage.xaml.cs
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.PlatformConfiguration;
|
||||||
|
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||||
|
using Entry = Xamarin.Forms.Entry;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class SendAddEditPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
|
|
||||||
|
private SendAddEditPageViewModel _vm;
|
||||||
|
|
||||||
|
public SendAddEditPage(
|
||||||
|
string sendId = null,
|
||||||
|
SendType? type = null)
|
||||||
|
{
|
||||||
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
|
InitializeComponent();
|
||||||
|
_vm = BindingContext as SendAddEditPageViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
_vm.SendId = sendId;
|
||||||
|
_vm.Type = type;
|
||||||
|
_vm.Init();
|
||||||
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
|
{
|
||||||
|
if (_vm.EditMode)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_removePassword);
|
||||||
|
ToolbarItems.Add(_copyLink);
|
||||||
|
ToolbarItems.Add(_shareLink);
|
||||||
|
ToolbarItems.Add(_deleteItem);
|
||||||
|
}
|
||||||
|
_vm.EditorMargins = new Thickness(0, 5, 0, 0);
|
||||||
|
}
|
||||||
|
else if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_closeItem);
|
||||||
|
if (_vm.EditMode)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_moreItem);
|
||||||
|
}
|
||||||
|
_vm.ShowEditorSeparators = true;
|
||||||
|
_vm.EditorMargins = new Thickness(0, 10, 0, 5);
|
||||||
|
_typePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
|
_deletionDateTypePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
|
_expirationDateTypePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
_typePicker.ItemDisplayBinding = new Binding("Key");
|
||||||
|
_deletionDateTypePicker.ItemDisplayBinding = new Binding("Key");
|
||||||
|
_expirationDateTypePicker.ItemDisplayBinding = new Binding("Key");
|
||||||
|
|
||||||
|
if (_vm.IsText)
|
||||||
|
{
|
||||||
|
_nameEntry.ReturnType = ReturnType.Next;
|
||||||
|
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
_broadcasterService.Subscribe(nameof(SendAddEditPage), message =>
|
||||||
|
{
|
||||||
|
if (message.Command == "selectFileResult")
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
var data = message.Data as Tuple<byte[], string>;
|
||||||
|
_vm.FileData = data.Item1;
|
||||||
|
_vm.FileName = data.Item2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
||||||
|
{
|
||||||
|
var success = await _vm.LoadAsync();
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_vm.EditMode && string.IsNullOrWhiteSpace(_vm.Send?.Name))
|
||||||
|
{
|
||||||
|
RequestFocus(_nameEntry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
base.OnDisappearing();
|
||||||
|
if (Device.RuntimePlatform != Device.iOS)
|
||||||
|
{
|
||||||
|
_broadcasterService.Unsubscribe(nameof(SendAddEditPage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(e.NewTextValue))
|
||||||
|
{
|
||||||
|
_vm.MaxAccessCount = null;
|
||||||
|
_maxAccessCountStepper.Value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// accept only digits
|
||||||
|
if (!int.TryParse(e.NewTextValue, out int _))
|
||||||
|
{
|
||||||
|
((Entry)sender).Text = e.OldTextValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ChooseFile_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await _vm.ChooseFileAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearExpirationDate_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
_vm.ClearExpirationDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Save_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await _vm.SubmitAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void RemovePassword_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await _vm.RemovePasswordAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CopyLink_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await _vm.CopyLinkAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ShareLink_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await _vm.ShareLinkAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Delete_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
if (await _vm.DeleteAsync())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void More_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!DoOnce())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var options = new List<string>();
|
||||||
|
if (_vm.Send.HasPassword)
|
||||||
|
{
|
||||||
|
options.Add(AppResources.RemovePassword);
|
||||||
|
}
|
||||||
|
if (_vm.EditMode)
|
||||||
|
{
|
||||||
|
options.Add(AppResources.CopyLink);
|
||||||
|
options.Add(AppResources.ShareLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||||
|
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
|
||||||
|
if (selection == AppResources.RemovePassword)
|
||||||
|
{
|
||||||
|
await _vm.RemovePasswordAsync();
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.CopyLink)
|
||||||
|
{
|
||||||
|
await _vm.CopyLinkAsync();
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.ShareLink)
|
||||||
|
{
|
||||||
|
await _vm.ShareLinkAsync();
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.Delete)
|
||||||
|
{
|
||||||
|
if (await _vm.DeleteAsync())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Close_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
510
src/App/Pages/Send/SendAddEditPageViewModel.cs
Normal file
510
src/App/Pages/Send/SendAddEditPageViewModel.cs
Normal file
|
@ -0,0 +1,510 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class SendAddEditPageViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private readonly ISendService _sendService;
|
||||||
|
private bool _canAccessPremium;
|
||||||
|
private SendView _send;
|
||||||
|
private bool _showEditorSeparators;
|
||||||
|
private Thickness _editorMargins;
|
||||||
|
private bool _showPassword;
|
||||||
|
private int _typeSelectedIndex;
|
||||||
|
private int _deletionDateTypeSelectedIndex;
|
||||||
|
private int _expirationDateTypeSelectedIndex;
|
||||||
|
private DateTime _deletionDate;
|
||||||
|
private TimeSpan _deletionTime;
|
||||||
|
private DateTime? _expirationDate;
|
||||||
|
private TimeSpan? _expirationTime;
|
||||||
|
private bool _isOverridingPickers;
|
||||||
|
private int? _maxAccessCount;
|
||||||
|
private string[] _additionalSendProperties = new []
|
||||||
|
{
|
||||||
|
nameof(IsText),
|
||||||
|
nameof(IsFile),
|
||||||
|
};
|
||||||
|
|
||||||
|
public SendAddEditPageViewModel()
|
||||||
|
{
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||||
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
|
|
||||||
|
TypeOptions = new List<KeyValuePair<string, SendType>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, SendType>(AppResources.TypeText, SendType.Text),
|
||||||
|
new KeyValuePair<string, SendType>(AppResources.TypeFile, SendType.File),
|
||||||
|
};
|
||||||
|
DeletionTypeOptions = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(AppResources.OneHour, AppResources.OneHour),
|
||||||
|
new KeyValuePair<string, string>(AppResources.OneDay, AppResources.OneDay),
|
||||||
|
new KeyValuePair<string, string>(AppResources.TwoDays, AppResources.TwoDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.ThreeDays, AppResources.ThreeDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.SevenDays, AppResources.SevenDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
||||||
|
};
|
||||||
|
ExpirationTypeOptions = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(AppResources.Never, AppResources.Never),
|
||||||
|
new KeyValuePair<string, string>(AppResources.OneHour, AppResources.OneHour),
|
||||||
|
new KeyValuePair<string, string>(AppResources.OneDay, AppResources.OneDay),
|
||||||
|
new KeyValuePair<string, string>(AppResources.TwoDays, AppResources.TwoDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.ThreeDays, AppResources.ThreeDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.SevenDays, AppResources.SevenDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
||||||
|
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Command TogglePasswordCommand { get; set; }
|
||||||
|
public string SendId { get; set; }
|
||||||
|
public SendType? Type { get; set; }
|
||||||
|
public string FileName { get; set; }
|
||||||
|
public byte[] FileData { get; set; }
|
||||||
|
public string NewPassword { get; set; }
|
||||||
|
public bool ShareOnSave { get; set; }
|
||||||
|
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
|
||||||
|
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
|
||||||
|
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
|
||||||
|
public int TypeSelectedIndex
|
||||||
|
{
|
||||||
|
get => _typeSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _typeSelectedIndex, value))
|
||||||
|
{
|
||||||
|
TypeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public int DeletionDateTypeSelectedIndex
|
||||||
|
{
|
||||||
|
get => _deletionDateTypeSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _deletionDateTypeSelectedIndex, value))
|
||||||
|
{
|
||||||
|
DeletionTypeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public DateTime DeletionDate
|
||||||
|
{
|
||||||
|
get => _deletionDate;
|
||||||
|
set => SetProperty(ref _deletionDate, value);
|
||||||
|
}
|
||||||
|
public TimeSpan DeletionTime
|
||||||
|
{
|
||||||
|
get => _deletionTime;
|
||||||
|
set => SetProperty(ref _deletionTime, value);
|
||||||
|
}
|
||||||
|
public int ExpirationDateTypeSelectedIndex
|
||||||
|
{
|
||||||
|
get => _expirationDateTypeSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _expirationDateTypeSelectedIndex, value))
|
||||||
|
{
|
||||||
|
ExpirationTypeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public DateTime? ExpirationDate
|
||||||
|
{
|
||||||
|
get => _expirationDate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _expirationDate, value))
|
||||||
|
{
|
||||||
|
ExpirationDateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public TimeSpan? ExpirationTime
|
||||||
|
{
|
||||||
|
get => _expirationTime;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _expirationTime, value))
|
||||||
|
{
|
||||||
|
ExpirationTimeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public int? MaxAccessCount
|
||||||
|
{
|
||||||
|
get => _maxAccessCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _maxAccessCount, value))
|
||||||
|
{
|
||||||
|
MaxAccessCountChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public SendView Send
|
||||||
|
{
|
||||||
|
get => _send;
|
||||||
|
set => SetProperty(ref _send, value, additionalPropertyNames: _additionalSendProperties);
|
||||||
|
}
|
||||||
|
public bool ShowEditorSeparators
|
||||||
|
{
|
||||||
|
get => _showEditorSeparators;
|
||||||
|
set => SetProperty(ref _showEditorSeparators, value);
|
||||||
|
}
|
||||||
|
public Thickness EditorMargins
|
||||||
|
{
|
||||||
|
get => _editorMargins;
|
||||||
|
set => SetProperty(ref _editorMargins, value);
|
||||||
|
}
|
||||||
|
public bool ShowPassword
|
||||||
|
{
|
||||||
|
get => _showPassword;
|
||||||
|
set => SetProperty(ref _showPassword, value,
|
||||||
|
additionalPropertyNames: new []
|
||||||
|
{
|
||||||
|
nameof(ShowPasswordIcon)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public bool EditMode => !string.IsNullOrWhiteSpace(SendId);
|
||||||
|
public bool IsText => Send?.Type == SendType.Text;
|
||||||
|
public bool IsFile => Send?.Type == SendType.File;
|
||||||
|
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
|
||||||
|
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
|
||||||
|
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
PageTitle = EditMode ? AppResources.EditSend : AppResources.AddSend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> LoadAsync()
|
||||||
|
{
|
||||||
|
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||||
|
_canAccessPremium = await userService.CanAccessPremiumAsync();
|
||||||
|
// TODO Policy Check
|
||||||
|
if (Send == null)
|
||||||
|
{
|
||||||
|
if (EditMode)
|
||||||
|
{
|
||||||
|
var send = await _sendService.GetAsync(SendId);
|
||||||
|
if (send == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Send = await send.DecryptAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Send = new SendView
|
||||||
|
{
|
||||||
|
Type = Type.GetValueOrDefault(SendType.Text),
|
||||||
|
DeletionDate = DateTime.Now.AddDays(7),
|
||||||
|
};
|
||||||
|
DeletionDateTypeSelectedIndex = 4;
|
||||||
|
ExpirationDateTypeSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeSelectedIndex = TypeOptions.FindIndex(k => k.Value == Send.Type);
|
||||||
|
MaxAccessCount = Send.MaxAccessCount;
|
||||||
|
_isOverridingPickers = true;
|
||||||
|
DeletionDate = Send.DeletionDate.ToLocalTime();
|
||||||
|
DeletionTime = DeletionDate.TimeOfDay;
|
||||||
|
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
|
||||||
|
ExpirationTime = ExpirationDate?.TimeOfDay;
|
||||||
|
_isOverridingPickers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChooseFileAsync()
|
||||||
|
{
|
||||||
|
await _deviceActionService.SelectFileAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearExpirationDate()
|
||||||
|
{
|
||||||
|
_isOverridingPickers = true;
|
||||||
|
ExpirationDate = null;
|
||||||
|
ExpirationTime = null;
|
||||||
|
_isOverridingPickers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSendData()
|
||||||
|
{
|
||||||
|
// filename
|
||||||
|
if (Send.File != null && FileName != null)
|
||||||
|
{
|
||||||
|
Send.File.FileName = FileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletion date
|
||||||
|
if (ShowDeletionCustomPickers)
|
||||||
|
{
|
||||||
|
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Send.DeletionDate = DeletionDate.ToUniversalTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// expiration date
|
||||||
|
if (ExpirationDate.HasValue)
|
||||||
|
{
|
||||||
|
if (ShowExpirationCustomPickers && ExpirationTime.HasValue)
|
||||||
|
{
|
||||||
|
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Send.ExpirationDate = ExpirationDate.Value.ToUniversalTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Send.ExpirationDate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SubmitAsync()
|
||||||
|
{
|
||||||
|
if (Send == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(Send.Name))
|
||||||
|
{
|
||||||
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||||
|
string.Format(AppResources.ValidationFieldRequired, AppResources.Name),
|
||||||
|
AppResources.Ok);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (IsFile)
|
||||||
|
{
|
||||||
|
if (!_canAccessPremium)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (FileData == null)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(
|
||||||
|
string.Format(AppResources.ValidationFieldRequired, AppResources.File),
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (FileData.Length > 104857600) // 100 MB
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.MaxFileSize,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSendData();
|
||||||
|
|
||||||
|
var (send, encryptedFileData) = await _sendService.EncryptAsync(Send, FileData, NewPassword);
|
||||||
|
if (send == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||||
|
var sendId = await _sendService.SaveWithServerAsync(send, encryptedFileData);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|
||||||
|
_platformUtilsService.ShowToast("success", null,
|
||||||
|
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
|
||||||
|
await Page.Navigation.PopModalAsync();
|
||||||
|
|
||||||
|
if (ShareOnSave)
|
||||||
|
{
|
||||||
|
var savedSend = await _sendService.GetAsync(sendId);
|
||||||
|
if (savedSend != null)
|
||||||
|
{
|
||||||
|
var savedSendView = await savedSend.DecryptAsync();
|
||||||
|
await AppHelpers.ShareSendUrl(savedSendView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ApiException e)
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
if (e?.Error != null)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemovePasswordAsync()
|
||||||
|
{
|
||||||
|
return await AppHelpers.RemoveSendPasswordAsync(SendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CopyLinkAsync()
|
||||||
|
{
|
||||||
|
await _platformUtilsService.CopyToClipboardAsync(AppHelpers.GetSendUrl(Send));
|
||||||
|
_platformUtilsService.ShowToast("info", null,
|
||||||
|
string.Format(AppResources.ValueHasBeenCopied, AppResources.ShareLink));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ShareLinkAsync()
|
||||||
|
{
|
||||||
|
await AppHelpers.ShareSendUrl(Send);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync()
|
||||||
|
{
|
||||||
|
return await AppHelpers.DeleteSendAsync(SendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void TypeChanged()
|
||||||
|
{
|
||||||
|
if (Send != null && TypeSelectedIndex > -1)
|
||||||
|
{
|
||||||
|
if (!EditMode && TypeOptions[TypeSelectedIndex].Value == SendType.File && !_canAccessPremium)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||||
|
TypeSelectedIndex = 0;
|
||||||
|
}
|
||||||
|
Send.Type = TypeOptions[TypeSelectedIndex].Value;
|
||||||
|
TriggerPropertyChanged(nameof(Send), _additionalSendProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeletionTypeChanged()
|
||||||
|
{
|
||||||
|
if (Send != null && DeletionDateTypeSelectedIndex > -1)
|
||||||
|
{
|
||||||
|
_isOverridingPickers = true;
|
||||||
|
switch (DeletionDateTypeSelectedIndex)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
DeletionDate = DateTime.Now.AddHours(1);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
DeletionDate = DateTime.Now.AddDays(1);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
DeletionDate = DateTime.Now.AddDays(2);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
DeletionDate = DateTime.Now.AddDays(3);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
case 6:
|
||||||
|
DeletionDate = DateTime.Now.AddDays(7);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
DeletionDate = DateTime.Now.AddDays(30);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
DeletionTime = DeletionDate.TimeOfDay;
|
||||||
|
_isOverridingPickers = false;
|
||||||
|
TriggerPropertyChanged(nameof(ShowDeletionCustomPickers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExpirationTypeChanged()
|
||||||
|
{
|
||||||
|
if (Send != null && ExpirationDateTypeSelectedIndex > -1)
|
||||||
|
{
|
||||||
|
_isOverridingPickers = true;
|
||||||
|
switch (ExpirationDateTypeSelectedIndex)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
ClearExpirationDate();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
ExpirationDate = DateTime.Now.AddHours(1);
|
||||||
|
ExpirationTime = ExpirationDate.Value.TimeOfDay;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
ExpirationDate = DateTime.Now.AddDays(1);
|
||||||
|
ExpirationTime = ExpirationDate.Value.TimeOfDay;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
ExpirationDate = DateTime.Now.AddDays(2);
|
||||||
|
ExpirationTime = ExpirationDate.Value.TimeOfDay;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
ExpirationDate = DateTime.Now.AddDays(3);
|
||||||
|
ExpirationTime = ExpirationDate.Value.TimeOfDay;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
ExpirationDate = DateTime.Now.AddDays(7);
|
||||||
|
ExpirationTime = ExpirationDate.Value.TimeOfDay;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
ExpirationDate = DateTime.Now.AddDays(30);
|
||||||
|
ExpirationTime = ExpirationDate.Value.TimeOfDay;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
ClearExpirationDate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_isOverridingPickers = false;
|
||||||
|
TriggerPropertyChanged(nameof(ShowExpirationCustomPickers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExpirationDateChanged()
|
||||||
|
{
|
||||||
|
if (!_isOverridingPickers && !ExpirationTime.HasValue)
|
||||||
|
{
|
||||||
|
// auto-set time to current time upon setting date
|
||||||
|
ExpirationTime = DateTime.Now.TimeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExpirationTimeChanged()
|
||||||
|
{
|
||||||
|
if (!_isOverridingPickers && !ExpirationDate.HasValue)
|
||||||
|
{
|
||||||
|
// auto-set date to current date upon setting time
|
||||||
|
ExpirationDate = DateTime.Today;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MaxAccessCountChanged()
|
||||||
|
{
|
||||||
|
Send.MaxAccessCount = _maxAccessCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TogglePassword()
|
||||||
|
{
|
||||||
|
ShowPassword = !ShowPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158
src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml
Normal file
158
src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.SendGroupingsPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:DataType="pages:SendGroupingsPageViewModel"
|
||||||
|
Title="{Binding PageTitle}"
|
||||||
|
x:Name="_page">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:SendGroupingsPageViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.ToolbarItems>
|
||||||
|
<ToolbarItem Icon="search.png" Clicked="Search_Clicked"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Search}" />
|
||||||
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
|
||||||
|
<ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}"
|
||||||
|
Clicked="Sync_Clicked" Order="Secondary" />
|
||||||
|
<ToolbarItem x:Name="_lockItem" x:Key="lockItem" Text="{u:I18n Lock}"
|
||||||
|
Clicked="Lock_Clicked" Order="Secondary" />
|
||||||
|
<ToolbarItem x:Name="_addItem" x:Key="addItem" Icon="plus.png"
|
||||||
|
Clicked="AddButton_Clicked" Order="Primary"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n AddItem}" />
|
||||||
|
|
||||||
|
<DataTemplate x:Key="sendTemplate"
|
||||||
|
x:DataType="pages:SendGroupingsPageListItem">
|
||||||
|
<controls:SendViewCell
|
||||||
|
Send="{Binding Send}"
|
||||||
|
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="sendGroupTemplate"
|
||||||
|
x:DataType="pages:SendGroupingsPageListItem">
|
||||||
|
<ViewCell>
|
||||||
|
<StackLayout Orientation="Horizontal"
|
||||||
|
StyleClass="list-row, list-row-platform">
|
||||||
|
<controls:FaLabel Text="{Binding Icon, Mode=OneWay}"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-icon, list-icon-platform">
|
||||||
|
<controls:FaLabel.Effects>
|
||||||
|
<effects:FixedSizeEffect />
|
||||||
|
</controls:FaLabel.Effects>
|
||||||
|
</controls:FaLabel>
|
||||||
|
<Label Text="{Binding Name, Mode=OneWay}"
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
StyleClass="list-title" />
|
||||||
|
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
HorizontalTextAlignment="End"
|
||||||
|
StyleClass="list-sub" />
|
||||||
|
</StackLayout>
|
||||||
|
</ViewCell>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<pages:SendGroupingsPageListItemSelector x:Key="sendListItemDataTemplateSelector"
|
||||||
|
SendTemplate="{StaticResource sendTemplate}"
|
||||||
|
GroupTemplate="{StaticResource sendGroupTemplate}" />
|
||||||
|
|
||||||
|
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
||||||
|
<StackLayout
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
Padding="20, 0"
|
||||||
|
Spacing="20"
|
||||||
|
IsVisible="{Binding ShowNoData}">
|
||||||
|
<Label
|
||||||
|
Text="{Binding NoDataText}"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n AddAnItem}"
|
||||||
|
Clicked="AddButton_Clicked"
|
||||||
|
IsVisible="{Binding ShowAddSendButton}" />
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<controls:ExtendedListView
|
||||||
|
x:Name="_listView"
|
||||||
|
IsVisible="{Binding ShowList}"
|
||||||
|
ItemsSource="{Binding GroupedSends}"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
HasUnevenRows="True"
|
||||||
|
RowHeight="-1"
|
||||||
|
RefreshCommand="{Binding RefreshCommand}"
|
||||||
|
IsPullToRefreshEnabled="True"
|
||||||
|
IsRefreshing="{Binding Refreshing}"
|
||||||
|
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
|
||||||
|
IsGroupingEnabled="True"
|
||||||
|
ItemSelected="RowSelected"
|
||||||
|
StyleClass="list, list-platform">
|
||||||
|
<x:Arguments>
|
||||||
|
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
|
||||||
|
</x:Arguments>
|
||||||
|
|
||||||
|
<ListView.GroupHeaderTemplate>
|
||||||
|
<DataTemplate x:DataType="pages:SendGroupingsPageListGroup">
|
||||||
|
<ViewCell>
|
||||||
|
<StackLayout
|
||||||
|
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
|
||||||
|
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||||
|
<BoxView
|
||||||
|
StyleClass="list-section-separator-top, list-section-separator-top-platform"
|
||||||
|
IsVisible="{Binding First, Converter={StaticResource inverseBool}}" />
|
||||||
|
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||||
|
<Label
|
||||||
|
Text="{Binding Name}"
|
||||||
|
StyleClass="list-header, list-header-platform" />
|
||||||
|
<Label
|
||||||
|
Text="{Binding ItemCount}"
|
||||||
|
StyleClass="list-header-sub" />
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView
|
||||||
|
StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||||
|
</StackLayout>
|
||||||
|
</ViewCell>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.GroupHeaderTemplate>
|
||||||
|
</controls:ExtendedListView>
|
||||||
|
</StackLayout>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
|
||||||
|
<AbsoluteLayout
|
||||||
|
x:Name="_absLayout"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
HorizontalOptions="FillAndExpand">
|
||||||
|
<ContentView
|
||||||
|
x:Name="_mainContent"
|
||||||
|
AbsoluteLayout.LayoutFlags="All"
|
||||||
|
AbsoluteLayout.LayoutBounds="0, 0, 1, 1" />
|
||||||
|
<Button
|
||||||
|
x:Name="_fab"
|
||||||
|
Image="plus.png"
|
||||||
|
Clicked="AddButton_Clicked"
|
||||||
|
Style="{StaticResource btn-fab}"
|
||||||
|
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||||
|
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n AddItem}">
|
||||||
|
<Button.Effects>
|
||||||
|
<effects:FabShadowEffect />
|
||||||
|
</Button.Effects>
|
||||||
|
</Button>
|
||||||
|
</AbsoluteLayout>
|
||||||
|
|
||||||
|
</pages:BaseContentPage>
|
188
src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs
Normal file
188
src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class SendGroupingsPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
|
private readonly ISyncService _syncService;
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
private readonly ISendService _sendService;
|
||||||
|
private readonly SendGroupingsPageViewModel _vm;
|
||||||
|
private readonly string _pageName;
|
||||||
|
|
||||||
|
private PreviousPageInfo _previousPage;
|
||||||
|
|
||||||
|
public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null,
|
||||||
|
PreviousPageInfo previousPage = null)
|
||||||
|
{
|
||||||
|
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||||
|
InitializeComponent();
|
||||||
|
ListView = _listView;
|
||||||
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
|
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||||
|
_vm = BindingContext as SendGroupingsPageViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
_vm.MainPage = mainPage;
|
||||||
|
_vm.Type = type;
|
||||||
|
_previousPage = previousPage;
|
||||||
|
if (pageTitle != null)
|
||||||
|
{
|
||||||
|
_vm.PageTitle = pageTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
_absLayout.Children.Remove(_fab);
|
||||||
|
ToolbarItems.Add(_addItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_syncItem);
|
||||||
|
ToolbarItems.Add(_lockItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtendedListView ListView { get; set; }
|
||||||
|
|
||||||
|
protected async override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
if (_syncService.SyncInProgress)
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||||
|
{
|
||||||
|
if (message.Command == "syncStarted")
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||||
|
}
|
||||||
|
else if (message.Command == "syncCompleted")
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
if (_vm.LoadedOnce)
|
||||||
|
{
|
||||||
|
var task = _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await LoadOnAppearedAsync(_mainLayout, false, async () =>
|
||||||
|
{
|
||||||
|
if (!_syncService.SyncInProgress || (await _sendService.GetAllAsync()).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e.Message.Contains("No key."))
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
await _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Task.Delay(5000);
|
||||||
|
if (!_vm.Loaded)
|
||||||
|
{
|
||||||
|
await _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShowPreviousPageAsync();
|
||||||
|
}, _mainContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
base.OnDisappearing();
|
||||||
|
IsBusy = false;
|
||||||
|
_broadcasterService.Unsubscribe(_pageName);
|
||||||
|
_vm.DisableRefreshing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||||
|
{
|
||||||
|
((ListView)sender).SelectedItem = null;
|
||||||
|
if (!DoOnce())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(e.SelectedItem is SendGroupingsPageListItem item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Send != null)
|
||||||
|
{
|
||||||
|
await _vm.SelectSendAsync(item.Send);
|
||||||
|
}
|
||||||
|
else if (item.Type != null)
|
||||||
|
{
|
||||||
|
await _vm.SelectTypeAsync(item.Type.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Search_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
var page = new SendsPage(_vm.Filter, _vm.Type != null);
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Sync_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
await _vm.SyncAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Lock_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
await _vaultTimeoutService.LockAsync(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void AddButton_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
var page = new SendAddEditPage(null, _vm.Type);
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowPreviousPageAsync()
|
||||||
|
{
|
||||||
|
if (_previousPage == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
|
||||||
|
{
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.SendId)));
|
||||||
|
}
|
||||||
|
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
|
||||||
|
{
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.SendId)));
|
||||||
|
}
|
||||||
|
_previousPage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class SendGroupingsPageListGroup : List<SendGroupingsPageListItem>
|
||||||
|
{
|
||||||
|
public SendGroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||||
|
: this(new List<SendGroupingsPageListItem>(), name, count, doUpper, first) { }
|
||||||
|
|
||||||
|
public SendGroupingsPageListGroup(List<SendGroupingsPageListItem> groupItems, string name, int count,
|
||||||
|
bool doUpper = true, bool first = false)
|
||||||
|
{
|
||||||
|
AddRange(groupItems);
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
Name = "-";
|
||||||
|
}
|
||||||
|
else if (doUpper)
|
||||||
|
{
|
||||||
|
Name = name.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
ItemCount = count > 0 ? count.ToString("N0") : "";
|
||||||
|
First = first;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool First { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString();
|
||||||
|
public string ItemCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class SendGroupingsPageListItem
|
||||||
|
{
|
||||||
|
private string _icon;
|
||||||
|
private string _name;
|
||||||
|
|
||||||
|
public SendView Send { get; set; }
|
||||||
|
public SendType? Type { get; set; }
|
||||||
|
public string ItemCount { get; set; }
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_name != null)
|
||||||
|
{
|
||||||
|
return _name;
|
||||||
|
}
|
||||||
|
if (Type != null)
|
||||||
|
{
|
||||||
|
switch (Type.Value)
|
||||||
|
{
|
||||||
|
case SendType.Text:
|
||||||
|
_name = AppResources.TypeText;
|
||||||
|
break;
|
||||||
|
case SendType.File:
|
||||||
|
_name = AppResources.TypeFile;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Icon
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_icon != null)
|
||||||
|
{
|
||||||
|
return _icon;
|
||||||
|
}
|
||||||
|
if (Type != null)
|
||||||
|
{
|
||||||
|
switch (Type.Value)
|
||||||
|
{
|
||||||
|
case SendType.Text:
|
||||||
|
_icon = "\uf0f6"; // fa-file-text-o
|
||||||
|
break;
|
||||||
|
case SendType.File:
|
||||||
|
_icon = "\uf016"; // fa-file-o
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class SendGroupingsPageListItemSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public DataTemplate SendTemplate { get; set; }
|
||||||
|
public DataTemplate GroupTemplate { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||||
|
{
|
||||||
|
if (item is SendGroupingsPageListItem listItem)
|
||||||
|
{
|
||||||
|
return listItem.Send != null ? SendTemplate : GroupTemplate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using DeviceType = Bit.Core.Enums.DeviceType;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class SendGroupingsPageViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private bool _refreshing;
|
||||||
|
private bool _doingLoad;
|
||||||
|
private bool _loading;
|
||||||
|
private bool _loaded;
|
||||||
|
private bool _showAddSendButton;
|
||||||
|
private bool _showNoData;
|
||||||
|
private bool _showList;
|
||||||
|
private bool _syncRefreshing;
|
||||||
|
private string _noDataText;
|
||||||
|
private List<SendView> _allSends;
|
||||||
|
private Dictionary<SendType, int> _typeCounts = new Dictionary<SendType, int>();
|
||||||
|
|
||||||
|
private readonly ISendService _sendService;
|
||||||
|
private readonly ISyncService _syncService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private readonly IStorageService _storageService;
|
||||||
|
|
||||||
|
public SendGroupingsPageViewModel()
|
||||||
|
{
|
||||||
|
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||||
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
|
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||||
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||||
|
|
||||||
|
Loading = true;
|
||||||
|
PageTitle = AppResources.Send;
|
||||||
|
GroupedSends = new ExtendedObservableCollection<SendGroupingsPageListGroup>();
|
||||||
|
RefreshCommand = new Command(async () =>
|
||||||
|
{
|
||||||
|
Refreshing = true;
|
||||||
|
await LoadAsync();
|
||||||
|
});
|
||||||
|
SendOptionsCommand = new Command<SendView>(SendOptionsAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MainPage { get; set; }
|
||||||
|
public SendType? Type { get; set; }
|
||||||
|
public Func<SendView, bool> Filter { get; set; }
|
||||||
|
public bool HasSends { get; set; }
|
||||||
|
public List<SendView> Sends { get; set; }
|
||||||
|
|
||||||
|
public bool Refreshing
|
||||||
|
{
|
||||||
|
get => _refreshing;
|
||||||
|
set => SetProperty(ref _refreshing, value);
|
||||||
|
}
|
||||||
|
public bool SyncRefreshing
|
||||||
|
{
|
||||||
|
get => _syncRefreshing;
|
||||||
|
set => SetProperty(ref _syncRefreshing, value);
|
||||||
|
}
|
||||||
|
public bool Loading
|
||||||
|
{
|
||||||
|
get => _loading;
|
||||||
|
set => SetProperty(ref _loading, value);
|
||||||
|
}
|
||||||
|
public bool Loaded
|
||||||
|
{
|
||||||
|
get => _loaded;
|
||||||
|
set => SetProperty(ref _loaded, value);
|
||||||
|
}
|
||||||
|
public bool ShowAddSendButton
|
||||||
|
{
|
||||||
|
get => _showAddSendButton;
|
||||||
|
set => SetProperty(ref _showAddSendButton, value);
|
||||||
|
}
|
||||||
|
public bool ShowNoData
|
||||||
|
{
|
||||||
|
get => _showNoData;
|
||||||
|
set => SetProperty(ref _showNoData, value);
|
||||||
|
}
|
||||||
|
public string NoDataText
|
||||||
|
{
|
||||||
|
get => _noDataText;
|
||||||
|
set => SetProperty(ref _noDataText, value);
|
||||||
|
}
|
||||||
|
public bool ShowList
|
||||||
|
{
|
||||||
|
get => _showList;
|
||||||
|
set => SetProperty(ref _showList, value);
|
||||||
|
}
|
||||||
|
public ExtendedObservableCollection<SendGroupingsPageListGroup> GroupedSends { get; set; }
|
||||||
|
public Command RefreshCommand { get; set; }
|
||||||
|
public Command<SendView> SendOptionsCommand { get; set; }
|
||||||
|
public bool LoadedOnce { get; set; }
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
if (_doingLoad)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var authed = await _userService.IsAuthenticatedAsync();
|
||||||
|
if (!authed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await _vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await _storageService.GetAsync<bool>(Constants.SyncOnRefreshKey) && Refreshing && !SyncRefreshing)
|
||||||
|
{
|
||||||
|
SyncRefreshing = true;
|
||||||
|
await _syncService.FullSyncAsync(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_doingLoad = true;
|
||||||
|
LoadedOnce = true;
|
||||||
|
ShowNoData = false;
|
||||||
|
Loading = true;
|
||||||
|
ShowList = false;
|
||||||
|
var groupedSends = new List<SendGroupingsPageListGroup>();
|
||||||
|
var page = Page as SendGroupingsPage;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LoadDataAsync();
|
||||||
|
|
||||||
|
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
||||||
|
if (MainPage)
|
||||||
|
{
|
||||||
|
groupedSends.Add(new SendGroupingsPageListGroup(
|
||||||
|
AppResources.Types, 0, uppercaseGroupNames, true)
|
||||||
|
{
|
||||||
|
new SendGroupingsPageListItem
|
||||||
|
{
|
||||||
|
Type = SendType.Text,
|
||||||
|
ItemCount = (_typeCounts.ContainsKey(SendType.Text) ?
|
||||||
|
_typeCounts[SendType.Text] : 0).ToString("N0")
|
||||||
|
},
|
||||||
|
new SendGroupingsPageListItem
|
||||||
|
{
|
||||||
|
Type = SendType.File,
|
||||||
|
ItemCount = (_typeCounts.ContainsKey(SendType.File) ?
|
||||||
|
_typeCounts[SendType.File] : 0).ToString("N0")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sends?.Any() ?? false)
|
||||||
|
{
|
||||||
|
var sendsListItems = Sends.Select(s => new SendGroupingsPageListItem { Send = s }).ToList();
|
||||||
|
groupedSends.Add(new SendGroupingsPageListGroup(sendsListItems,
|
||||||
|
MainPage ? AppResources.AllSends : AppResources.Sends, sendsListItems.Count,
|
||||||
|
uppercaseGroupNames, !MainPage));
|
||||||
|
}
|
||||||
|
GroupedSends.ResetWithRange(groupedSends);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_doingLoad = false;
|
||||||
|
Loaded = true;
|
||||||
|
Loading = false;
|
||||||
|
ShowNoData = (MainPage && !HasSends) || !groupedSends.Any();
|
||||||
|
ShowList = !ShowNoData;
|
||||||
|
DisableRefreshing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisableRefreshing()
|
||||||
|
{
|
||||||
|
Refreshing = false;
|
||||||
|
SyncRefreshing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SelectSendAsync(SendView send)
|
||||||
|
{
|
||||||
|
var page = new SendAddEditPage(send.Id);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SelectTypeAsync(SendType type)
|
||||||
|
{
|
||||||
|
string title = null;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case SendType.Text:
|
||||||
|
title = AppResources.TypeText;
|
||||||
|
break;
|
||||||
|
case SendType.File:
|
||||||
|
title = AppResources.TypeFile;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var page = new SendGroupingsPage(false, type, title);
|
||||||
|
await Page.Navigation.PushAsync(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SyncAsync()
|
||||||
|
{
|
||||||
|
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _syncService.FullSyncAsync(false, true);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
_platformUtilsService.ShowToast("error", null, AppResources.SyncingFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadDataAsync()
|
||||||
|
{
|
||||||
|
NoDataText = AppResources.NoSends;
|
||||||
|
_allSends = await _sendService.GetAllDecryptedAsync();
|
||||||
|
HasSends = _allSends.Any();
|
||||||
|
_typeCounts.Clear();
|
||||||
|
Filter = null;
|
||||||
|
|
||||||
|
if (MainPage)
|
||||||
|
{
|
||||||
|
Sends = _allSends;
|
||||||
|
foreach (var c in _allSends)
|
||||||
|
{
|
||||||
|
if (_typeCounts.ContainsKey(c.Type))
|
||||||
|
{
|
||||||
|
_typeCounts[c.Type] = _typeCounts[c.Type] + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_typeCounts.Add(c.Type, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Type != null)
|
||||||
|
{
|
||||||
|
Filter = c => c.Type == Type.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PageTitle = AppResources.AllSends;
|
||||||
|
}
|
||||||
|
Sends = Filter != null ? _allSends.Where(Filter).ToList() : _allSends;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SendOptionsAsync(SendView send)
|
||||||
|
{
|
||||||
|
if ((Page as BaseContentPage).DoOnce())
|
||||||
|
{
|
||||||
|
var selection = await AppHelpers.SendListOptions(Page, send);
|
||||||
|
if (selection == AppResources.RemovePassword || selection == AppResources.Delete)
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
src/App/Pages/Send/SendsPage.xaml
Normal file
81
src/App/Pages/Send/SendsPage.xaml
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.SendsPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
|
||||||
|
x:DataType="pages:SendsPageViewModel"
|
||||||
|
x:Name="_page"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:SendsPageViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:DateTimeConverter x:Key="dateTime" />
|
||||||
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
|
x:Name="_closeItem" x:Key="closeItem" />
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
Spacing="0"
|
||||||
|
Padding="0"
|
||||||
|
x:Name="_titleLayout"
|
||||||
|
x:Key="titleLayout">
|
||||||
|
<controls:MiButton
|
||||||
|
StyleClass="btn-title, btn-title-platform"
|
||||||
|
Text=""
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
Clicked="BackButton_Clicked"
|
||||||
|
x:Name="_backButton" />
|
||||||
|
<controls:ExtendedSearchBar
|
||||||
|
x:Name="_searchBar"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
TextChanged="SearchBar_TextChanged"
|
||||||
|
SearchButtonPressed="SearchBar_SearchButtonPressed"
|
||||||
|
Placeholder="{Binding PageTitle}" />
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform"
|
||||||
|
x:Name="_separator" x:Key="separator" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
|
||||||
|
<StackLayout x:Name="_mainLayout" Spacing="0" Padding="0">
|
||||||
|
<controls:FaLabel IsVisible="{Binding ShowSearchDirection}"
|
||||||
|
Text=""
|
||||||
|
StyleClass="text-muted"
|
||||||
|
FontSize="50"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
HorizontalOptions="CenterAndExpand"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<Label IsVisible="{Binding ShowNoData}"
|
||||||
|
Text="{u:I18n NoItemsToList}"
|
||||||
|
Margin="20, 0"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
HorizontalOptions="CenterAndExpand"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<ListView x:Name="_listView"
|
||||||
|
IsVisible="{Binding ShowList}"
|
||||||
|
ItemsSource="{Binding Sends}"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
HasUnevenRows="true"
|
||||||
|
CachingStrategy="RecycleElement"
|
||||||
|
ItemSelected="RowSelected"
|
||||||
|
StyleClass="list, list-platform">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="views:SendView">
|
||||||
|
<controls:SendViewCell
|
||||||
|
Send="{Binding .}"
|
||||||
|
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
</pages:BaseContentPage>
|
109
src/App/Pages/Send/SendsPage.xaml.cs
Normal file
109
src/App/Pages/Send/SendsPage.xaml.cs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class SendsPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private SendsPageViewModel _vm;
|
||||||
|
private bool _hasFocused;
|
||||||
|
|
||||||
|
public SendsPage(Func<SendView, bool> filter, bool type = false)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_vm = BindingContext as SendsPageViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
_vm.Filter = filter;
|
||||||
|
if (type)
|
||||||
|
{
|
||||||
|
_vm.PageTitle = AppResources.SearchType;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_vm.PageTitle = AppResources.SearchSends;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_closeItem);
|
||||||
|
_searchBar.Placeholder = AppResources.Search;
|
||||||
|
_mainLayout.Children.Insert(0, _searchBar);
|
||||||
|
_mainLayout.Children.Insert(1, _separator);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NavigationPage.SetTitleView(this, _titleLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchBar SearchBar => _searchBar;
|
||||||
|
|
||||||
|
protected async override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
await _vm.InitAsync();
|
||||||
|
if (!_hasFocused)
|
||||||
|
{
|
||||||
|
_hasFocused = true;
|
||||||
|
RequestFocus(_searchBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var oldLength = e.OldTextValue?.Length ?? 0;
|
||||||
|
var newLength = e.NewTextValue?.Length ?? 0;
|
||||||
|
if (oldLength < 2 && newLength < 2 && oldLength < newLength)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_vm.Search(e.NewTextValue, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_vm.Search((sender as SearchBar).Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BackButton_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
GoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnBackButtonPressed()
|
||||||
|
{
|
||||||
|
GoBack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoBack()
|
||||||
|
{
|
||||||
|
if (!DoOnce())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigation.PopModalAsync(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||||
|
{
|
||||||
|
((ListView)sender).SelectedItem = null;
|
||||||
|
if (!DoOnce())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.SelectedItem is SendView send)
|
||||||
|
{
|
||||||
|
await _vm.SelectSendAsync(send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
GoBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
120
src/App/Pages/Send/SendsPageViewModel.cs
Normal file
120
src/App/Pages/Send/SendsPageViewModel.cs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class SendsPageViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private readonly ISearchService _searchService;
|
||||||
|
|
||||||
|
private CancellationTokenSource _searchCancellationTokenSource;
|
||||||
|
private bool _showNoData;
|
||||||
|
private bool _showList;
|
||||||
|
|
||||||
|
public SendsPageViewModel()
|
||||||
|
{
|
||||||
|
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||||
|
Sends = new ExtendedObservableCollection<SendView>();
|
||||||
|
SendOptionsCommand = new Command<SendView>(SendOptionsAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Command SendOptionsCommand { get; set; }
|
||||||
|
public ExtendedObservableCollection<SendView> Sends { get; set; }
|
||||||
|
public Func<SendView, bool> Filter { get; set; }
|
||||||
|
|
||||||
|
public bool ShowNoData
|
||||||
|
{
|
||||||
|
get => _showNoData;
|
||||||
|
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new []
|
||||||
|
{
|
||||||
|
nameof(ShowSearchDirection)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowList
|
||||||
|
{
|
||||||
|
get => _showList;
|
||||||
|
set => SetProperty(ref _showList, value, additionalPropertyNames: new []
|
||||||
|
{
|
||||||
|
nameof(ShowSearchDirection)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowSearchDirection => !ShowList && !ShowNoData;
|
||||||
|
|
||||||
|
public async Task InitAsync()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace((Page as SendsPage).SearchBar.Text))
|
||||||
|
{
|
||||||
|
Search((Page as SendsPage).SearchBar.Text, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Search(string searchText, int? timeout = null)
|
||||||
|
{
|
||||||
|
var previousCts = _searchCancellationTokenSource;
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
List<SendView> sends = null;
|
||||||
|
var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1;
|
||||||
|
if (searchable)
|
||||||
|
{
|
||||||
|
if (timeout != null)
|
||||||
|
{
|
||||||
|
await Task.Delay(timeout.Value);
|
||||||
|
}
|
||||||
|
if (searchText != (Page as SendsPage).SearchBar.Text)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
previousCts?.Cancel();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sends = await _searchService.SearchSendsAsync(searchText, Filter, null, cts.Token);
|
||||||
|
cts.Token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sends == null)
|
||||||
|
{
|
||||||
|
sends = new List<SendView>();
|
||||||
|
}
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
Sends.ResetWithRange(sends);
|
||||||
|
ShowNoData = searchable && Sends.Count == 0;
|
||||||
|
ShowList = searchable && !ShowNoData;
|
||||||
|
});
|
||||||
|
}, cts.Token);
|
||||||
|
_searchCancellationTokenSource = cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SelectSendAsync(SendView send)
|
||||||
|
{
|
||||||
|
var page = new SendAddEditPage(send.Id);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SendOptionsAsync(SendView send)
|
||||||
|
{
|
||||||
|
if ((Page as BaseContentPage).DoOnce())
|
||||||
|
{
|
||||||
|
await AppHelpers.SendListOptions(Page, send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ namespace Bit.App.Pages
|
||||||
public class TabsPage : TabbedPage
|
public class TabsPage : TabbedPage
|
||||||
{
|
{
|
||||||
private NavigationPage _groupingsPage;
|
private NavigationPage _groupingsPage;
|
||||||
|
private NavigationPage _sendGroupingsPage;
|
||||||
private NavigationPage _generatorPage;
|
private NavigationPage _generatorPage;
|
||||||
|
|
||||||
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
|
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
|
||||||
|
@ -19,6 +20,13 @@ namespace Bit.App.Pages
|
||||||
};
|
};
|
||||||
Children.Add(_groupingsPage);
|
Children.Add(_groupingsPage);
|
||||||
|
|
||||||
|
_sendGroupingsPage = new NavigationPage(new SendGroupingsPage(true))
|
||||||
|
{
|
||||||
|
Title = AppResources.Send,
|
||||||
|
IconImageSource = "paper_plane.png",
|
||||||
|
};
|
||||||
|
Children.Add(_sendGroupingsPage);
|
||||||
|
|
||||||
_generatorPage = new NavigationPage(new GeneratorPage(true, null, this))
|
_generatorPage = new NavigationPage(new GeneratorPage(true, null, this))
|
||||||
{
|
{
|
||||||
Title = AppResources.Generator,
|
Title = AppResources.Generator,
|
||||||
|
|
258
src/App/Resources/AppResources.Designer.cs
generated
258
src/App/Resources/AppResources.Designer.cs
generated
|
@ -3182,5 +3182,263 @@ namespace Bit.App.Resources {
|
||||||
return ResourceManager.GetString("PersonalOwnershipPolicyInEffect", resourceCulture);
|
return ResourceManager.GetString("PersonalOwnershipPolicyInEffect", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string Send {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Send", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AllSends {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AllSends", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Sends {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Sends", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TypeText {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TypeText", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string HideTextByDefault {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("HideTextByDefault", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TypeFile {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TypeFile", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DeletionDate {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeletionDate", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DeletionTime {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeletionTime", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DeletionDateInfo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeletionDateInfo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string PendingDelete {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("PendingDelete", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExpirationDate {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ExpirationDate", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExpirationTime {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ExpirationTime", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExpirationDateInfo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ExpirationDateInfo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Expired {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Expired", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MaximumAccessCount {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MaximumAccessCount", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MaximumAccessCountInfo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MaximumAccessCountInfo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MaximumAccessCountReached {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("MaximumAccessCountReached", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CurrentAccessCount {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("CurrentAccessCount", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NewPassword {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NewPassword", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string PasswordInfo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("PasswordInfo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RemovePassword {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("RemovePassword", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AreYouSureRemoveSendPassword {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AreYouSureRemoveSendPassword", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RemovingSendPassword {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("RemovingSendPassword", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SendPasswordRemoved {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SendPasswordRemoved", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NotesInfo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NotesInfo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DisableSend {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DisableSend", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NoSends {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NoSends", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CopyLink {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("CopyLink", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ShareLink {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ShareLink", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SearchSends {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SearchSends", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string EditSend {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("EditSend", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AddSend {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AddSend", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AreYouSureDeleteSend {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AreYouSureDeleteSend", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SendDeleted {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SendDeleted", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SendUpdated {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SendUpdated", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NewSendCreated {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NewSendCreated", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string OneDay {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("OneDay", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string TwoDays {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TwoDays", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ThreeDays {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThreeDays", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SevenDays {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SevenDays", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ThirtyDays {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThirtyDays", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Custom {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Custom", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ShareOnSave {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ShareOnSave", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1799,4 +1799,152 @@
|
||||||
<data name="PersonalOwnershipPolicyInEffect" xml:space="preserve">
|
<data name="PersonalOwnershipPolicyInEffect" xml:space="preserve">
|
||||||
<value>An organization policy is affecting your ownership options.</value>
|
<value>An organization policy is affecting your ownership options.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Send" xml:space="preserve">
|
||||||
|
<value>Send</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="AllSends" xml:space="preserve">
|
||||||
|
<value>All Sends</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Sends" xml:space="preserve">
|
||||||
|
<value>Sends</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="TypeText" xml:space="preserve">
|
||||||
|
<value>Text</value>
|
||||||
|
</data>
|
||||||
|
<data name="HideTextByDefault" xml:space="preserve">
|
||||||
|
<value>When accessing the Send, hide the text by default</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="TypeFile" xml:space="preserve">
|
||||||
|
<value>File</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
|
<value>Deletion Date</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeletionTime" xml:space="preserve">
|
||||||
|
<value>Deletion Time</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeletionDateInfo" xml:space="preserve">
|
||||||
|
<value>The Send will be permanently deleted on the specified date and time.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PendingDelete" xml:space="preserve">
|
||||||
|
<value>Pending deletion</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpirationDate" xml:space="preserve">
|
||||||
|
<value>Expiration Date</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpirationTime" xml:space="preserve">
|
||||||
|
<value>Expiration Time</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpirationDateInfo" xml:space="preserve">
|
||||||
|
<value>If set, access to this Send will expire on the specified date and time.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Expired" xml:space="preserve">
|
||||||
|
<value>Expired</value>
|
||||||
|
</data>
|
||||||
|
<data name="MaximumAccessCount" xml:space="preserve">
|
||||||
|
<value>Maximum Access Count</value>
|
||||||
|
</data>
|
||||||
|
<data name="MaximumAccessCountInfo" xml:space="preserve">
|
||||||
|
<value>If set, users will no longer be able to access this send once the maximum access count is reached.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="MaximumAccessCountReached" xml:space="preserve">
|
||||||
|
<value>Max access count reached</value>
|
||||||
|
</data>
|
||||||
|
<data name="CurrentAccessCount" xml:space="preserve">
|
||||||
|
<value>Current Access Count</value>
|
||||||
|
</data>
|
||||||
|
<data name="NewPassword" xml:space="preserve">
|
||||||
|
<value>New Password</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordInfo" xml:space="preserve">
|
||||||
|
<value>Optionally require a password for users to access this Send.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="RemovePassword" xml:space="preserve">
|
||||||
|
<value>Remove Password</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureRemoveSendPassword" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to remove the password?</value>
|
||||||
|
</data>
|
||||||
|
<data name="RemovingSendPassword" xml:space="preserve">
|
||||||
|
<value>Removing password</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendPasswordRemoved" xml:space="preserve">
|
||||||
|
<value>Password has been removed.</value>
|
||||||
|
</data>
|
||||||
|
<data name="NotesInfo" xml:space="preserve">
|
||||||
|
<value>Private notes about this Send.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="DisableSend" xml:space="preserve">
|
||||||
|
<value>Disable this Send so that no one can access it.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="NoSends" xml:space="preserve">
|
||||||
|
<value>There are no sends in your account.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="CopyLink" xml:space="preserve">
|
||||||
|
<value>Copy Link</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShareLink" xml:space="preserve">
|
||||||
|
<value>Share Link</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchSends" xml:space="preserve">
|
||||||
|
<value>Search sends</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="EditSend" xml:space="preserve">
|
||||||
|
<value>Edit Send</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="AddSend" xml:space="preserve">
|
||||||
|
<value>Add Send</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureDeleteSend" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to delete this Send?</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="SendDeleted" xml:space="preserve">
|
||||||
|
<value>Send has been deleted.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="SendUpdated" xml:space="preserve">
|
||||||
|
<value>Send updated.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="NewSendCreated" xml:space="preserve">
|
||||||
|
<value>New send created.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
|
<data name="OneDay" xml:space="preserve">
|
||||||
|
<value>1 day</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoDays" xml:space="preserve">
|
||||||
|
<value>2 days</value>
|
||||||
|
</data>
|
||||||
|
<data name="ThreeDays" xml:space="preserve">
|
||||||
|
<value>3 days</value>
|
||||||
|
</data>
|
||||||
|
<data name="SevenDays" xml:space="preserve">
|
||||||
|
<value>7 days</value>
|
||||||
|
</data>
|
||||||
|
<data name="ThirtyDays" xml:space="preserve">
|
||||||
|
<value>30 days</value>
|
||||||
|
</data>
|
||||||
|
<data name="Custom" xml:space="preserve">
|
||||||
|
<value>Custom</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShareOnSave" xml:space="preserve">
|
||||||
|
<value>Share this Send upon save.</value>
|
||||||
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -359,4 +359,11 @@
|
||||||
<Setter Property="Margin"
|
<Setter Property="Margin"
|
||||||
Value="0, -10, 0, 0" />
|
Value="0, -10, 0, 0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style TargetType="DatePicker"
|
||||||
|
Class="datetime-picker">
|
||||||
|
<Setter Property="FontSize"
|
||||||
|
Value="Medium" />
|
||||||
|
<Setter Property="TextColor"
|
||||||
|
Value="{StaticResource TextColor}" />
|
||||||
|
</Style>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Bit.App.Abstractions;
|
using System;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Pages;
|
using Bit.App.Pages;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
@ -9,6 +10,8 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Utilities
|
namespace Bit.App.Utilities
|
||||||
|
@ -130,6 +133,143 @@ namespace Bit.App.Utilities
|
||||||
return selection;
|
return selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<string> SendListOptions(ContentPage page, SendView send)
|
||||||
|
{
|
||||||
|
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
|
var options = new List<string> { AppResources.Edit };
|
||||||
|
options.Add(AppResources.CopyLink);
|
||||||
|
options.Add(AppResources.ShareLink);
|
||||||
|
if (send.HasPassword)
|
||||||
|
{
|
||||||
|
options.Add(AppResources.RemovePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection = await page.DisplayActionSheet(send.Name, AppResources.Cancel, AppResources.Delete,
|
||||||
|
options.ToArray());
|
||||||
|
if (await vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked);
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.Edit)
|
||||||
|
{
|
||||||
|
await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(send.Id)));
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.CopyLink)
|
||||||
|
{
|
||||||
|
await platformUtilsService.CopyToClipboardAsync(GetSendUrl(send));
|
||||||
|
platformUtilsService.ShowToast("info", null,
|
||||||
|
string.Format(AppResources.ValueHasBeenCopied, AppResources.ShareLink));
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.ShareLink)
|
||||||
|
{
|
||||||
|
await ShareSendUrl(send);
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.RemovePassword)
|
||||||
|
{
|
||||||
|
await RemoveSendPasswordAsync(send.Id);
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.Delete)
|
||||||
|
{
|
||||||
|
await DeleteSendAsync(send.Id);
|
||||||
|
}
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSendUrl(SendView send)
|
||||||
|
{
|
||||||
|
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
|
return environmentService.BaseUrl + "/#/send/" + send.AccessId + "/" + send.UrlB64Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task ShareSendUrl(SendView send)
|
||||||
|
{
|
||||||
|
await Share.RequestAsync(new ShareTextRequest
|
||||||
|
{
|
||||||
|
Uri = new Uri(GetSendUrl(send)).ToString(),
|
||||||
|
Title = AppResources.Send + " " + send.Name,
|
||||||
|
Subject = send.Name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> RemoveSendPasswordAsync(string sendId)
|
||||||
|
{
|
||||||
|
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||||
|
|
||||||
|
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var confirmed = await platformUtilsService.ShowDialogAsync(
|
||||||
|
AppResources.AreYouSureRemoveSendPassword,
|
||||||
|
null, AppResources.Yes, AppResources.Cancel);
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await deviceActionService.ShowLoadingAsync(AppResources.RemovingSendPassword);
|
||||||
|
await sendService.RemovePasswordWithServerAsync(sendId);
|
||||||
|
await deviceActionService.HideLoadingAsync();
|
||||||
|
platformUtilsService.ShowToast("success", null, AppResources.SendPasswordRemoved);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ApiException e)
|
||||||
|
{
|
||||||
|
await deviceActionService.HideLoadingAsync();
|
||||||
|
if (e?.Error != null)
|
||||||
|
{
|
||||||
|
await platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> DeleteSendAsync(string sendId)
|
||||||
|
{
|
||||||
|
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||||
|
|
||||||
|
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var confirmed = await platformUtilsService.ShowDialogAsync(
|
||||||
|
AppResources.AreYouSureDeleteSend,
|
||||||
|
null, AppResources.Yes, AppResources.Cancel);
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await deviceActionService.ShowLoadingAsync(AppResources.Deleting);
|
||||||
|
await sendService.DeleteWithServerAsync(sendId);
|
||||||
|
await deviceActionService.HideLoadingAsync();
|
||||||
|
platformUtilsService.ShowToast("success", null, AppResources.SendDeleted);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ApiException e)
|
||||||
|
{
|
||||||
|
await deviceActionService.HideLoadingAsync();
|
||||||
|
if (e?.Error != null)
|
||||||
|
{
|
||||||
|
await platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<bool> PerformUpdateTasksAsync(ISyncService syncService,
|
public static async Task<bool> PerformUpdateTasksAsync(ISyncService syncService,
|
||||||
IDeviceActionService deviceActionService, IStorageService storageService)
|
IDeviceActionService deviceActionService, IStorageService storageService)
|
||||||
{
|
{
|
||||||
|
|
|
@ -15,5 +15,9 @@ namespace Bit.Core.Abstractions
|
||||||
List<CipherView> ciphers = null, CancellationToken ct = default);
|
List<CipherView> ciphers = null, CancellationToken ct = default);
|
||||||
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
|
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
|
||||||
CancellationToken ct = default, bool deleted = false);
|
CancellationToken ct = default, bool deleted = false);
|
||||||
|
Task<List<SendView>> SearchSendsAsync(string query, Func<SendView, bool> filter = null,
|
||||||
|
List<SendView> sends = null, CancellationToken ct = default);
|
||||||
|
List<SendView> SearchSendsBasic(List<SendView> sends, string query,
|
||||||
|
CancellationToken ct = default, bool deleted = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@ namespace Bit.Core.Abstractions
|
||||||
public interface ISendService
|
public interface ISendService
|
||||||
{
|
{
|
||||||
void ClearCache();
|
void ClearCache();
|
||||||
Task<(Send send, CipherString encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password,
|
Task<(Send send, byte[] encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password,
|
||||||
SymmetricCryptoKey key = null);
|
SymmetricCryptoKey key = null);
|
||||||
Task<Send> GetAsync(string id);
|
Task<Send> GetAsync(string id);
|
||||||
Task<List<Send>> GetAllAsync();
|
Task<List<Send>> GetAllAsync();
|
||||||
Task<List<SendView>> GetAllDecryptedAsync();
|
Task<List<SendView>> GetAllDecryptedAsync();
|
||||||
Task SaveWithServerAsync(Send sendData, byte[] encryptedFileData);
|
Task<string> SaveWithServerAsync(Send sendData, byte[] encryptedFileData);
|
||||||
Task UpsertAsync(params SendData[] send);
|
Task UpsertAsync(params SendData[] send);
|
||||||
Task ReplaceAsync(Dictionary<string, SendData> sends);
|
Task ReplaceAsync(Dictionary<string, SendData> sends);
|
||||||
Task ClearAsync(string userId);
|
Task ClearAsync(string userId);
|
||||||
|
|
|
@ -15,10 +15,10 @@ namespace Bit.Core.Models.Response
|
||||||
public SendTextApi Text { get; set; }
|
public SendTextApi Text { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public int? MaxAccessCount { get; set; }
|
public int? MaxAccessCount { get; set; }
|
||||||
public int AccessCount { get; internal set; }
|
public int AccessCount { get; set; }
|
||||||
public DateTime RevisionDate { get; internal set; }
|
public DateTime RevisionDate { get; set; }
|
||||||
public DateTime? ExpirationDate { get; internal set; }
|
public DateTime? ExpirationDate { get; set; }
|
||||||
public DateTime DeletionDate { get; internal set; }
|
public DateTime DeletionDate { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ namespace Bit.Core.Models.View
|
||||||
{
|
{
|
||||||
public class SendView : View
|
public class SendView : View
|
||||||
{
|
{
|
||||||
|
public SendView() { }
|
||||||
|
|
||||||
public SendView(Send send) : base()
|
public SendView(Send send) : base()
|
||||||
{
|
{
|
||||||
Id = send.Id;
|
Id = send.Id;
|
||||||
|
@ -38,8 +40,10 @@ namespace Bit.Core.Models.View
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
public string UrlB64Key => Key == null ? null : CoreHelpers.Base64UrlEncode(Key);
|
public string UrlB64Key => Key == null ? null : CoreHelpers.Base64UrlEncode(Key);
|
||||||
|
public bool HasPassword => Password?.Length > 0;
|
||||||
public bool MaxAccessCountReached => MaxAccessCount.HasValue && AccessCount >= MaxAccessCount.Value;
|
public bool MaxAccessCountReached => MaxAccessCount.HasValue && AccessCount >= MaxAccessCount.Value;
|
||||||
public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow;
|
public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow;
|
||||||
public bool PendingDelete => DeletionDate <= DateTime.UtcNow;
|
public bool PendingDelete => DeletionDate <= DateTime.UtcNow;
|
||||||
|
public string DisplayDate => DeletionDate.ToLocalTime().ToString("MMM d, yyyy, h:mm tt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,7 @@ namespace Bit.Core.Services
|
||||||
SendAsync<SendRequest, SendResponse>(HttpMethod.Put, $"/sends/{id}", request, true, true);
|
SendAsync<SendRequest, SendResponse>(HttpMethod.Put, $"/sends/{id}", request, true, true);
|
||||||
|
|
||||||
public Task<SendResponse> PutSendRemovePasswordAsync(string id) =>
|
public Task<SendResponse> PutSendRemovePasswordAsync(string id) =>
|
||||||
SendAsync<object, SendResponse>(HttpMethod.Put, $"/sends/{id}", null, true, true);
|
SendAsync<object, SendResponse>(HttpMethod.Put, $"/sends/{id}/remove-password", null, true, true);
|
||||||
|
|
||||||
public Task DeleteSendAsync(string id) =>
|
public Task DeleteSendAsync(string id) =>
|
||||||
SendAsync<object, object>(HttpMethod.Delete, $"/sends/{id}", null, true, false);
|
SendAsync<object, object>(HttpMethod.Delete, $"/sends/{id}", null, true, false);
|
||||||
|
|
|
@ -429,7 +429,7 @@ namespace Bit.Core.Services
|
||||||
|
|
||||||
public async Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial)
|
public async Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial)
|
||||||
{
|
{
|
||||||
var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 65, HkdfAlgorithm.Sha256);
|
var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 64, HkdfAlgorithm.Sha256);
|
||||||
return new SymmetricCryptoKey(sendKey);
|
return new SymmetricCryptoKey(sendKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,14 @@ namespace Bit.Core.Services
|
||||||
public class SearchService : ISearchService
|
public class SearchService : ISearchService
|
||||||
{
|
{
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
|
private readonly ISendService _sendService;
|
||||||
|
|
||||||
public SearchService(
|
public SearchService(
|
||||||
ICipherService cipherService)
|
ICipherService cipherService,
|
||||||
|
ISendService sendService)
|
||||||
{
|
{
|
||||||
_cipherService = cipherService;
|
_cipherService = cipherService;
|
||||||
|
_sendService = sendService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearIndex()
|
public void ClearIndex()
|
||||||
|
@ -94,5 +97,61 @@ namespace Bit.Core.Services
|
||||||
return false;
|
return false;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<SendView>> SearchSendsAsync(string query, Func<SendView, bool> filter = null,
|
||||||
|
List<SendView> sends = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var results = new List<SendView>();
|
||||||
|
if (query != null)
|
||||||
|
{
|
||||||
|
query = query.Trim().ToLower();
|
||||||
|
}
|
||||||
|
if (query == string.Empty)
|
||||||
|
{
|
||||||
|
query = null;
|
||||||
|
}
|
||||||
|
if (sends == null)
|
||||||
|
{
|
||||||
|
sends = await _sendService.GetAllDecryptedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
sends = sends.Where(filter).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (!IsSearchable(query))
|
||||||
|
{
|
||||||
|
return sends;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SearchSendsBasic(sends, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SendView> SearchSendsBasic(List<SendView> sends, string query, CancellationToken ct = default,
|
||||||
|
bool deleted = false)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
query = query.Trim().ToLower();
|
||||||
|
return sends.Where(s =>
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (s.Name?.ToLower().Contains(query) ?? false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (s.Text?.Text?.ToLower().Contains(query) ?? false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (s.File?.FileName?.ToLower()?.Contains(query) ?? false)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ namespace Bit.Core.Services
|
||||||
await DeleteAsync(id);
|
await DeleteAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(Send send, CipherString encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
|
public async Task<(Send send, byte[] encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
|
||||||
string password, SymmetricCryptoKey key = null)
|
string password, SymmetricCryptoKey key = null)
|
||||||
{
|
{
|
||||||
if (model.Key == null)
|
if (model.Key == null)
|
||||||
|
@ -91,17 +91,20 @@ namespace Bit.Core.Services
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
Type = model.Type,
|
Type = model.Type,
|
||||||
Disabled = model.Disabled,
|
Disabled = model.Disabled,
|
||||||
|
DeletionDate = model.DeletionDate,
|
||||||
|
ExpirationDate = model.ExpirationDate,
|
||||||
MaxAccessCount = model.MaxAccessCount,
|
MaxAccessCount = model.MaxAccessCount,
|
||||||
Key = await _cryptoService.EncryptAsync(model.Key, key),
|
Key = await _cryptoService.EncryptAsync(model.Key, key),
|
||||||
Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey),
|
Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey),
|
||||||
Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey),
|
Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey),
|
||||||
};
|
};
|
||||||
CipherString encryptedFileData = null;
|
byte[] encryptedFileData = null;
|
||||||
|
|
||||||
if (password != null)
|
if (password != null)
|
||||||
{
|
{
|
||||||
|
var kdfIterations = await _userService.GetKdfIterationsAsync() ?? 100000;
|
||||||
var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key,
|
var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key,
|
||||||
CryptoHashAlgorithm.Sha256, 100000);
|
CryptoHashAlgorithm.Sha256, kdfIterations);
|
||||||
send.Password = Convert.ToBase64String(passwordHash);
|
send.Password = Convert.ToBase64String(passwordHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +122,7 @@ namespace Bit.Core.Services
|
||||||
if (fileData != null)
|
if (fileData != null)
|
||||||
{
|
{
|
||||||
send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey);
|
send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey);
|
||||||
encryptedFileData = await _cryptoService.EncryptAsync(fileData, model.CryptoKey);
|
encryptedFileData = await _cryptoService.EncryptToBytesAsync(fileData, model.CryptoKey);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -133,7 +136,7 @@ namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
var userId = await _userService.GetUserIdAsync();
|
var userId = await _userService.GetUserIdAsync();
|
||||||
var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId));
|
var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId));
|
||||||
return sends.Select(kvp => new Send(kvp.Value)).ToList();
|
return sends?.Select(kvp => new Send(kvp.Value)).ToList() ?? new List<Send>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<SendView>> GetAllDecryptedAsync()
|
public async Task<List<SendView>> GetAllDecryptedAsync()
|
||||||
|
@ -161,7 +164,7 @@ namespace Bit.Core.Services
|
||||||
async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync());
|
async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync());
|
||||||
await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s)));
|
await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s)));
|
||||||
|
|
||||||
decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList();
|
decSends = decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList();
|
||||||
_decryptedSendsCache = decSends;
|
_decryptedSendsCache = decSends;
|
||||||
return _decryptedSendsCache;
|
return _decryptedSendsCache;
|
||||||
}
|
}
|
||||||
|
@ -190,9 +193,8 @@ namespace Bit.Core.Services
|
||||||
_decryptedSendsCache = null;
|
_decryptedSendsCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveWithServerAsync(Send send, byte[] encryptedFileData)
|
public async Task<string> SaveWithServerAsync(Send send, byte[] encryptedFileData)
|
||||||
{
|
{
|
||||||
|
|
||||||
var request = new SendRequest(send);
|
var request = new SendRequest(send);
|
||||||
SendResponse response;
|
SendResponse response;
|
||||||
if (send.Id == null)
|
if (send.Id == null)
|
||||||
|
@ -223,6 +225,7 @@ namespace Bit.Core.Services
|
||||||
|
|
||||||
var userId = await _userService.GetUserIdAsync();
|
var userId = await _userService.GetUserIdAsync();
|
||||||
await UpsertAsync(new SendData(response, userId));
|
await UpsertAsync(new SendData(response, userId));
|
||||||
|
return response.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpsertAsync(params SendData[] sends)
|
public async Task UpsertAsync(params SendData[] sends)
|
||||||
|
|
|
@ -374,7 +374,11 @@ namespace Bit.Core.Services
|
||||||
await _policyService.Replace(policies);
|
await _policyService.Replace(policies);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SyncSendsAsync(string userId, List<SendResponse> sends) =>
|
private async Task SyncSendsAsync(string userId, List<SendResponse> response)
|
||||||
_sendService.ReplaceAsync(sends.ToDictionary(s => userId, s => new SendData(s, userId)));
|
{
|
||||||
|
var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ??
|
||||||
|
new Dictionary<string, SendData>();
|
||||||
|
await _sendService.ReplaceAsync(sends);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,9 @@ namespace Bit.Core.Utilities
|
||||||
var folderService = new FolderService(cryptoService, userService, apiService, storageService,
|
var folderService = new FolderService(cryptoService, userService, apiService, storageService,
|
||||||
i18nService, cipherService);
|
i18nService, cipherService);
|
||||||
var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
||||||
searchService = new SearchService(cipherService);
|
var sendService = new SendService(cryptoService, userService, apiService, storageService, i18nService,
|
||||||
|
cryptoFunctionService);
|
||||||
|
searchService = new SearchService(cipherService, sendService);
|
||||||
var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService,
|
var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService,
|
||||||
storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService,
|
storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService,
|
||||||
null, (expired) =>
|
null, (expired) =>
|
||||||
|
@ -54,8 +56,6 @@ namespace Bit.Core.Utilities
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
});
|
});
|
||||||
var policyService = new PolicyService(storageService, userService);
|
var policyService = new PolicyService(storageService, userService);
|
||||||
var sendService = new SendService(cryptoService, userService, apiService, storageService, i18nService,
|
|
||||||
cryptoFunctionService);
|
|
||||||
var syncService = new SyncService(userService, apiService, settingsService, folderService,
|
var syncService = new SyncService(userService, apiService, settingsService, folderService,
|
||||||
cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService,
|
cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService,
|
||||||
(bool expired) =>
|
(bool expired) =>
|
||||||
|
@ -84,6 +84,7 @@ namespace Bit.Core.Utilities
|
||||||
Register<ICipherService>("cipherService", cipherService);
|
Register<ICipherService>("cipherService", cipherService);
|
||||||
Register<IFolderService>("folderService", folderService);
|
Register<IFolderService>("folderService", folderService);
|
||||||
Register<ICollectionService>("collectionService", collectionService);
|
Register<ICollectionService>("collectionService", collectionService);
|
||||||
|
Register<ISendService>("sendService", sendService);
|
||||||
Register<ISearchService>("searchService", searchService);
|
Register<ISearchService>("searchService", searchService);
|
||||||
Register<IPolicyService>("policyService", policyService);
|
Register<IPolicyService>("policyService", policyService);
|
||||||
Register<ISyncService>("syncService", syncService);
|
Register<ISyncService>("syncService", syncService);
|
||||||
|
|
59
src/iOS.Core/Renderers/ExtendedDatePickerRenderer.cs
Normal file
59
src/iOS.Core/Renderers/ExtendedDatePickerRenderer.cs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.iOS.Core.Renderers;
|
||||||
|
using UIKit;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.iOS;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(ExtendedDatePicker), typeof(ExtendedDatePickerRenderer))]
|
||||||
|
namespace Bit.iOS.Core.Renderers
|
||||||
|
{
|
||||||
|
public class ExtendedDatePickerRenderer : DatePickerRenderer
|
||||||
|
{
|
||||||
|
protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e)
|
||||||
|
{
|
||||||
|
base.OnElementChanged(e);
|
||||||
|
if (Control != null && Element is ExtendedDatePicker element)
|
||||||
|
{
|
||||||
|
// center text
|
||||||
|
Control.TextAlignment = UITextAlignment.Center;
|
||||||
|
|
||||||
|
// use placeholder until NullableDate set
|
||||||
|
if (!element.NullableDate.HasValue)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// force use of wheel picker on iOS 14+
|
||||||
|
// TODO remove this when we upgrade to X.F 5 SR-1
|
||||||
|
// https://github.com/xamarin/Xamarin.Forms/issues/12258#issuecomment-700168665
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 2))
|
||||||
|
{
|
||||||
|
var picker = (UIDatePicker)Control.InputView;
|
||||||
|
picker.PreferredDatePickerStyle = UIDatePickerStyle.Wheels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == DatePicker.DateProperty.PropertyName ||
|
||||||
|
e.PropertyName == DatePicker.FormatProperty.PropertyName)
|
||||||
|
{
|
||||||
|
if (Control != null && Element is ExtendedDatePicker element)
|
||||||
|
{
|
||||||
|
if (Element.Format == element.PlaceHolder)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnElementPropertyChanged(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/iOS.Core/Renderers/ExtendedTimePickerRenderer.cs
Normal file
59
src/iOS.Core/Renderers/ExtendedTimePickerRenderer.cs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.iOS.Core.Renderers;
|
||||||
|
using UIKit;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.iOS;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(ExtendedTimePicker), typeof(ExtendedTimePickerRenderer))]
|
||||||
|
namespace Bit.iOS.Core.Renderers
|
||||||
|
{
|
||||||
|
public class ExtendedTimePickerRenderer : TimePickerRenderer
|
||||||
|
{
|
||||||
|
protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e)
|
||||||
|
{
|
||||||
|
base.OnElementChanged(e);
|
||||||
|
if (Control != null && Element is ExtendedTimePicker element)
|
||||||
|
{
|
||||||
|
// center text
|
||||||
|
Control.TextAlignment = UITextAlignment.Center;
|
||||||
|
|
||||||
|
// use placeholder until NullableTime set
|
||||||
|
if (!element.NullableTime.HasValue)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// force use of wheel picker on iOS 14+
|
||||||
|
// TODO remove this when we upgrade to X.F 5 SR-1
|
||||||
|
// https://github.com/xamarin/Xamarin.Forms/issues/12258#issuecomment-700168665
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 2))
|
||||||
|
{
|
||||||
|
var picker = (UIDatePicker)Control.InputView;
|
||||||
|
picker.PreferredDatePickerStyle = UIDatePickerStyle.Wheels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == TimePicker.TimeProperty.PropertyName ||
|
||||||
|
e.PropertyName == TimePicker.FormatProperty.PropertyName)
|
||||||
|
{
|
||||||
|
if (Control != null && Element is ExtendedTimePicker element)
|
||||||
|
{
|
||||||
|
if (Element.Format == element.PlaceHolder)
|
||||||
|
{
|
||||||
|
Control.Text = element.PlaceHolder;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnElementPropertyChanged(sender, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -155,12 +155,14 @@
|
||||||
<Compile Include="NFCReaderDelegate.cs" />
|
<Compile Include="NFCReaderDelegate.cs" />
|
||||||
<Compile Include="Renderers\CustomButtonRenderer.cs" />
|
<Compile Include="Renderers\CustomButtonRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomContentPageRenderer.cs" />
|
<Compile Include="Renderers\CustomContentPageRenderer.cs" />
|
||||||
|
<Compile Include="Renderers\ExtendedDatePickerRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomEditorRenderer.cs" />
|
<Compile Include="Renderers\CustomEditorRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomEntryRenderer.cs" />
|
<Compile Include="Renderers\CustomEntryRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomLabelRenderer.cs" />
|
<Compile Include="Renderers\CustomLabelRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomPickerRenderer.cs" />
|
<Compile Include="Renderers\CustomPickerRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomSearchBarRenderer.cs" />
|
<Compile Include="Renderers\CustomSearchBarRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomTabbedRenderer.cs" />
|
<Compile Include="Renderers\CustomTabbedRenderer.cs" />
|
||||||
|
<Compile Include="Renderers\ExtendedTimePickerRenderer.cs" />
|
||||||
<Compile Include="Renderers\CustomViewCellRenderer.cs" />
|
<Compile Include="Renderers\CustomViewCellRenderer.cs" />
|
||||||
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
|
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
|
||||||
<Compile Include="Services\BiometricService.cs" />
|
<Compile Include="Services\BiometricService.cs" />
|
||||||
|
|
BIN
src/iOS/Resources/paper_plane.png
Normal file
BIN
src/iOS/Resources/paper_plane.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 898 B |
BIN
src/iOS/Resources/paper_plane@2x.png
Normal file
BIN
src/iOS/Resources/paper_plane@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
src/iOS/Resources/paper_plane@3x.png
Normal file
BIN
src/iOS/Resources/paper_plane@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -267,6 +267,15 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<BundleResource Include="Resources\logo%403x.png" />
|
<BundleResource Include="Resources\logo%403x.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<BundleResource Include="Resources\paper_plane.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<BundleResource Include="Resources\paper_plane%402x.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<BundleResource Include="Resources\paper_plane%403x.png" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<BundleResource Include="Resources\plus.png" />
|
<BundleResource Include="Resources\plus.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -339,6 +339,8 @@ namespace Bit.Core.Test.Services
|
||||||
new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
|
new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
|
||||||
CipherString encrypt(string secret, SymmetricCryptoKey key) =>
|
CipherString encrypt(string secret, SymmetricCryptoKey key) =>
|
||||||
new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
|
new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
|
||||||
|
byte[] encryptFileBytes(byte[] secret, SymmetricCryptoKey key) =>
|
||||||
|
secret.Concat(key.Key).ToArray();
|
||||||
|
|
||||||
sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>())
|
sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>())
|
||||||
.Returns(info => getPbkdf((string)info[0], (byte[])info[1]));
|
.Returns(info => getPbkdf((string)info[0], (byte[])info[1]));
|
||||||
|
@ -346,12 +348,14 @@ namespace Bit.Core.Test.Services
|
||||||
.Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
|
.Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
|
||||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<string>(), Arg.Any<SymmetricCryptoKey>())
|
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<string>(), Arg.Any<SymmetricCryptoKey>())
|
||||||
.Returns(info => encrypt((string)info[0], (SymmetricCryptoKey)info[1]));
|
.Returns(info => encrypt((string)info[0], (SymmetricCryptoKey)info[1]));
|
||||||
|
sutProvider.GetDependency<ICryptoService>().EncryptToBytesAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
|
||||||
|
.Returns(info => encryptFileBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
|
||||||
|
|
||||||
var (send, encryptedFileData) = await sutProvider.Sut.EncryptAsync(view, fileData, view.Password, privateKey);
|
var (send, encryptedFileData) = await sutProvider.Sut.EncryptAsync(view, fileData, view.Password, privateKey);
|
||||||
|
|
||||||
TestHelper.AssertPropertyEqual(view, send, "Password", "Key", "Name", "Notes", "Text", "File",
|
TestHelper.AssertPropertyEqual(view, send, "Password", "Key", "Name", "Notes", "Text", "File",
|
||||||
"AccessCount", "AccessId", "CryptoKey", "RevisionDate", "DeletionDate", "ExpirationDate", "UrlB64Key",
|
"AccessCount", "AccessId", "CryptoKey", "RevisionDate", "DeletionDate", "ExpirationDate", "UrlB64Key",
|
||||||
"MaxAccessCountReached", "Expired", "PendingDelete");
|
"MaxAccessCountReached", "Expired", "PendingDelete", "HasPassword", "DisplayDate");
|
||||||
Assert.Equal(Convert.ToBase64String(getPbkdf(view.Password, view.Key)), send.Password);
|
Assert.Equal(Convert.ToBase64String(getPbkdf(view.Password, view.Key)), send.Password);
|
||||||
TestHelper.AssertPropertyEqual(encryptBytes(view.Key, privateKey), send.Key);
|
TestHelper.AssertPropertyEqual(encryptBytes(view.Key, privateKey), send.Key);
|
||||||
TestHelper.AssertPropertyEqual(encrypt(view.Name, view.CryptoKey), send.Name);
|
TestHelper.AssertPropertyEqual(encrypt(view.Name, view.CryptoKey), send.Name);
|
||||||
|
@ -366,7 +370,7 @@ namespace Bit.Core.Test.Services
|
||||||
case SendType.File:
|
case SendType.File:
|
||||||
// Only set filename
|
// Only set filename
|
||||||
TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName);
|
TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName);
|
||||||
TestHelper.AssertPropertyEqual(encryptBytes(fileData, view.CryptoKey), encryptedFileData);
|
Assert.Equal(encryptFileBytes(fileData, view.CryptoKey), encryptedFileData);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Exception("Untested send type");
|
throw new Exception("Untested send type");
|
||||||
|
|
Loading…
Reference in a new issue