Update 2FA and Captcha controls
This commit is contained in:
parent
1f1f34b6ce
commit
ded58f687d
@ -2,7 +2,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Version>9.4.0.1</Version>
|
<Version>9.3.0.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Octokit" Version="5.0.0" />
|
<PackageReference Include="Octokit" Version="5.0.0" />
|
||||||
|
|||||||
@ -18,7 +18,7 @@ namespace AudibleUtilities
|
|||||||
public class ApiExtended
|
public class ApiExtended
|
||||||
{
|
{
|
||||||
public Api Api { get; private set; }
|
public Api Api { get; private set; }
|
||||||
private const string DeviceName = "Libation";
|
|
||||||
private ApiExtended(Api api) => Api = api;
|
private ApiExtended(Api api) => Api = api;
|
||||||
|
|
||||||
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
|
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
|
||||||
|
|||||||
@ -35,13 +35,13 @@
|
|||||||
Text="Password:" />
|
Text="Password:" />
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
|
Name="passwordBox"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
Margin="0,10,0,0"
|
Margin="0,10,0,0"
|
||||||
PasswordChar="*"
|
PasswordChar="*"
|
||||||
Text="{Binding Password}"
|
Text="{Binding Password, Mode=TwoWay}" />
|
||||||
IsEnabled="{Binding Password, Converter={x:Static StringConverters.IsNullOrEmpty}}" />
|
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
@ -51,10 +51,11 @@
|
|||||||
Text="CAPTCHA
answer:" />
|
Text="CAPTCHA
answer:" />
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
|
Name="captchaBox"
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="0,10,0,0"
|
Margin="0,10,0,0"
|
||||||
Text="{Binding Answer}" />
|
Text="{Binding Answer, Mode=TwoWay}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Grid.Row="4"
|
Grid.Row="4"
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using LibationAvalonia.ViewModels;
|
using LibationAvalonia.ViewModels;
|
||||||
@ -12,10 +13,12 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
public string Password => _viewModel.Password;
|
public string Password => _viewModel.Password;
|
||||||
public string Answer => _viewModel.Answer;
|
public string Answer => _viewModel.Answer;
|
||||||
|
|
||||||
private CaptchaDialogViewModel _viewModel;
|
private readonly CaptchaDialogViewModel _viewModel;
|
||||||
public CaptchaDialog()
|
public CaptchaDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));
|
||||||
|
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptchaDialog(string password, byte[] captchaImage) :this()
|
public CaptchaDialog(string password, byte[] captchaImage) :this()
|
||||||
@ -25,19 +28,25 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
|
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
|
||||||
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
|
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
|
||||||
var gifFrames = new Bitmap[gif.Frames.Count];
|
var gifFrames = new Bitmap[gif.Frames.Count];
|
||||||
|
var frameDelayMs = new int[gif.Frames.Count];
|
||||||
|
|
||||||
for (int i = 0; i < gif.Frames.Count; i++)
|
for (int i = 0; i < gif.Frames.Count; i++)
|
||||||
{
|
{
|
||||||
using var framems = new MemoryStream();
|
var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
|
||||||
|
|
||||||
using var clonedFrame = gif.Frames.CloneFrame(i);
|
using var clonedFrame = gif.Frames.CloneFrame(i);
|
||||||
|
using var framems = new MemoryStream();
|
||||||
|
|
||||||
clonedFrame.Save(framems, gifEncoder);
|
clonedFrame.Save(framems, gifEncoder);
|
||||||
framems.Position = 0;
|
framems.Position = 0;
|
||||||
|
|
||||||
gifFrames[i] = new Bitmap(framems);
|
gifFrames[i] = new Bitmap(framems);
|
||||||
|
frameDelayMs[i] = frameMetadata.FrameDelay * 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataContext = _viewModel = new(password, gifFrames);
|
DataContext = _viewModel = new(password, gifFrames, frameDelayMs);
|
||||||
|
|
||||||
|
Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
@ -59,6 +68,12 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
await base.SaveAndCloseAsync();
|
await base.SaveAndCloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task CancelAndCloseAsync()
|
||||||
|
{
|
||||||
|
await _viewModel.StopAsync();
|
||||||
|
await base.CancelAndCloseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> await SaveAndCloseAsync();
|
=> await SaveAndCloseAsync();
|
||||||
}
|
}
|
||||||
@ -72,14 +87,11 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
private Bitmap _captchaImage;
|
private Bitmap _captchaImage;
|
||||||
private bool keepSwitching = true;
|
private bool keepSwitching = true;
|
||||||
private readonly Task FrameSwitch;
|
private readonly Task FrameSwitch;
|
||||||
private readonly Bitmap[] GifFrames;
|
|
||||||
private const int FRAME_INTERVAL_MS = 100;
|
|
||||||
|
|
||||||
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames)
|
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames, int[] frameDelayMs)
|
||||||
{
|
{
|
||||||
Password = password;
|
Password = password;
|
||||||
GifFrames = gifFrames;
|
FrameSwitch = SwitchFramesAsync(gifFrames, frameDelayMs);
|
||||||
FrameSwitch = SwitchFramesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
@ -88,16 +100,19 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
await FrameSwitch;
|
await FrameSwitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SwitchFramesAsync()
|
private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
|
||||||
{
|
{
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while(keepSwitching)
|
while(keepSwitching)
|
||||||
{
|
{
|
||||||
CaptchaImage = GifFrames[index++];
|
CaptchaImage = gifFrames[index];
|
||||||
|
await Task.Delay(frameDelayMs[index++]);
|
||||||
|
|
||||||
index %= GifFrames.Length;
|
index %= gifFrames.Length;
|
||||||
await Task.Delay(FRAME_INTERVAL_MS);
|
}
|
||||||
}
|
|
||||||
|
foreach (var frame in gifFrames)
|
||||||
|
frame.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,12 @@
|
|||||||
Title="2FA Code"
|
Title="2FA Code"
|
||||||
Icon="/Assets/libation.ico">
|
Icon="/Assets/libation.ico">
|
||||||
|
|
||||||
<Grid VerticalAlignment="Stretch" RowDefinitions="*,Auto,Auto,Auto">
|
<Grid
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
ColumnDefinitions="*" Margin="5"
|
||||||
|
RowDefinitions="*,Auto,Auto,Auto">
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="5"
|
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Text="{Binding Prompt}" />
|
Text="{Binding Prompt}" />
|
||||||
@ -24,14 +26,15 @@
|
|||||||
Text="Enter 2FA Code" />
|
Text="Enter 2FA Code" />
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
|
Name="_2FABox"
|
||||||
Margin="5,0,5,0"
|
Margin="5,0,5,0"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
Text="{Binding Code, Mode=TwoWay}" />
|
Text="{Binding Code, Mode=TwoWay}" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
Margin="5"
|
Margin="5"
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
VerticalAlignment="Bottom"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
Content="Submit"
|
Content="Submit"
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -8,20 +7,20 @@ namespace LibationAvalonia.Dialogs.Login
|
|||||||
public partial class _2faCodeDialog : DialogWindow
|
public partial class _2faCodeDialog : DialogWindow
|
||||||
{
|
{
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
public string Prompt { get; }
|
public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
|
||||||
|
|
||||||
|
|
||||||
public _2faCodeDialog() => InitializeComponent();
|
public _2faCodeDialog()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
|
||||||
|
}
|
||||||
|
|
||||||
public _2faCodeDialog(string prompt) : this()
|
public _2faCodeDialog(string prompt) : this()
|
||||||
{
|
{
|
||||||
Prompt = prompt;
|
Prompt = prompt;
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
}
|
Opened += (_, _) => _2FABox.Focus();
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task SaveAndCloseAsync()
|
protected override Task SaveAndCloseAsync()
|
||||||
|
|||||||
@ -24,10 +24,8 @@ namespace LibationWinForms.Dialogs.Login
|
|||||||
this.captchaPb.Image = image;
|
this.captchaPb.Image = image;
|
||||||
|
|
||||||
passwordTb.Text = password;
|
passwordTb.Text = password;
|
||||||
passwordTb.Enabled = string.IsNullOrEmpty(password);
|
|
||||||
|
|
||||||
if (passwordTb.Enabled)
|
(string.IsNullOrEmpty(password) ? passwordTb : answerTb).Select();
|
||||||
answerTb.Select();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void submitBtn_Click(object sender, EventArgs e)
|
private void submitBtn_Click(object sender, EventArgs e)
|
||||||
|
|||||||
@ -47,7 +47,7 @@ namespace LibationWinForms
|
|||||||
// load init state to menu checkbox
|
// load init state to menu checkbox
|
||||||
Load += updateAutoScanLibraryToolStripMenuItem;
|
Load += updateAutoScanLibraryToolStripMenuItem;
|
||||||
// if enabled: begin on load
|
// if enabled: begin on load
|
||||||
Load += startAutoScan;
|
Shown += startAutoScan;
|
||||||
|
|
||||||
// if new 'default' account is added, run autoscan
|
// if new 'default' account is added, run autoscan
|
||||||
AccountsSettingsPersister.Saving += accountsPreSave;
|
AccountsSettingsPersister.Saving += accountsPreSave;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user