Use xplat webview control for Audible login

- Use Avalonia-based webview control for Audible login with Chardonnay
- Remove webview interfaces from IInteropFunctions
- Remove Microsoft.Web.WebView2 package from WindowsConfigApp
- Add Microsoft.Web.WebView2 to LibationWinForms
- Remove all other login forms except the external login dialog (fallback in case webview doesn't work). The AudibleApi login with username/password doesn't work anymore. Need to use external browser login method.
This commit is contained in:
Michael Bucari-Tovo
2025-11-03 09:35:12 -07:00
parent f98adef9e9
commit fa238a0915
53 changed files with 137 additions and 2786 deletions

View File

@@ -113,6 +113,7 @@ Essential: no
Priority: optional
Maintainer: github.com/rmcrackan
Description: liberate your audiobooks
Recommends: libgtk-3-0, libwebkit2gtk-4.1-0
" >> $FOLDER_DEBIAN/control
echo "Changing permissions for pre- and post-install files..."

View File

@@ -62,7 +62,7 @@ License: GPLv3+
URL: https://github.com/rmcrackan/Libation
Source0: https://github.com/rmcrackan/Libation
Requires: bash
Requires: bash gtk3 webkit2gtk4.1
%define __os_install_post %{nil}

View File

@@ -23,11 +23,6 @@ namespace LibationAvalonia
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Task<DialogResult> ShowDialogAsync(this Dialogs.Login.WebLoginDialog dialogWindow, Window? owner = null)
=> ((owner ?? App.MainWindow) is Window window)
? dialogWindow.ShowDialog<DialogResult>(window)
: Task.FromResult(DialogResult.None);
public static Window? GetParentWindow(this Control control) => control.GetVisualRoot() as Window;

View File

@@ -1,176 +0,0 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia;
using LibationFileManager;
using System;
using System.Threading.Tasks;
namespace LibationAvalonia.Controls;
#nullable enable
public class NativeWebView : NativeControlHost, IWebView
{
private IWebViewAdapter? _webViewAdapter;
private Uri? _delayedSource;
private TaskCompletionSource _webViewReadyCompletion = new();
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
public event EventHandler? DOMContentLoaded;
public bool CanGoBack => _webViewAdapter?.CanGoBack ?? false;
public bool CanGoForward => _webViewAdapter?.CanGoForward ?? false;
public Uri? Source
{
get => _webViewAdapter?.Source ?? throw new InvalidOperationException("Control was not initialized");
set
{
if (_webViewAdapter is null)
{
_delayedSource = value;
return;
}
_webViewAdapter.Source = value;
}
}
public bool GoBack()
{
return _webViewAdapter?.GoBack() ?? throw new InvalidOperationException("Control was not initialized");
}
public bool GoForward()
{
return _webViewAdapter?.GoForward() ?? throw new InvalidOperationException("Control was not initialized");
}
public Task<string?> InvokeScriptAsync(string scriptName)
{
return _webViewAdapter is null
? throw new InvalidOperationException("Control was not initialized")
: _webViewAdapter.InvokeScriptAsync(scriptName);
}
public void Navigate(Uri url)
{
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.Navigate(url);
}
public Task NavigateToString(string text)
{
return (_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.NavigateToString(text);
}
public void Refresh()
{
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.Refresh();
}
public void Stop()
{
(_webViewAdapter ?? throw new InvalidOperationException("Control was not initialized"))
.Stop();
}
public Task WaitForNativeHost()
{
return _webViewReadyCompletion.Task;
}
private class PlatformHandle : IPlatformHandle
{
public nint Handle { get; init; }
public string? HandleDescriptor { get; init; }
}
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
_webViewAdapter = InteropFactory.Create().CreateWebViewAdapter();
if (_webViewAdapter is null)
return base.CreateNativeControlCore(parent);
else
{
SubscribeOnEvents();
var handle = new PlatformHandle
{
Handle = _webViewAdapter.PlatformHandle.Handle,
HandleDescriptor = _webViewAdapter.PlatformHandle.HandleDescriptor
};
if (_delayedSource is not null)
{
_webViewAdapter.Source = _delayedSource;
}
_webViewReadyCompletion.TrySetResult();
return handle;
}
}
private void SubscribeOnEvents()
{
if (_webViewAdapter is not null)
{
_webViewAdapter.NavigationStarted += WebViewAdapterOnNavigationStarted;
_webViewAdapter.NavigationCompleted += WebViewAdapterOnNavigationCompleted;
_webViewAdapter.DOMContentLoaded += _webViewAdapter_DOMContentLoaded;
}
}
private void _webViewAdapter_DOMContentLoaded(object? sender, EventArgs e)
{
DOMContentLoaded?.Invoke(this, e);
}
private void WebViewAdapterOnNavigationStarted(object? sender, WebViewNavigationEventArgs e)
{
NavigationStarted?.Invoke(this, e);
}
private void WebViewAdapterOnNavigationCompleted(object? sender, WebViewNavigationEventArgs e)
{
NavigationCompleted?.Invoke(this, e);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BoundsProperty && change.NewValue is Rect rect)
{
var scaling = (float)(VisualRoot?.RenderScaling ?? 1.0f);
_webViewAdapter?.HandleResize((int)(rect.Width * scaling), (int)(rect.Height * scaling), scaling);
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (_webViewAdapter != null)
{
e.Handled = _webViewAdapter.HandleKeyDown((uint)e.Key, (uint)e.KeyModifiers);
}
base.OnKeyDown(e);
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
if (_webViewAdapter is not null)
{
_webViewReadyCompletion = new TaskCompletionSource();
_webViewAdapter.NavigationStarted -= WebViewAdapterOnNavigationStarted;
_webViewAdapter.NavigationCompleted -= WebViewAdapterOnNavigationCompleted;
(_webViewAdapter as IDisposable)?.Dispose();
}
}
}

View File

@@ -1,34 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="140"
MinWidth="240" MinHeight="140"
MaxWidth="240" MaxHeight="140"
Width="240" Height="140"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
Title="Approval Alert Detected">
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock
Grid.Row="0"
Margin="10"
TextWrapping="Wrap"
Text="Amazon is sending you an email."/>
<TextBlock
Grid.Row="1" Margin="10,0,10,0"
TextWrapping="Wrap"
Text="Please press this button after you've approved the notification."/>
<Button
Grid.Row="2"
Margin="10"
VerticalAlignment="Bottom"
Padding="30,3,30,3"
Content="Approve"
Click="Approve_Click" />
</Grid>
</Window>

View File

@@ -1,22 +0,0 @@
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class ApprovalNeededDialog : DialogWindow
{
public ApprovalNeededDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Approve button clicked");
return base.SaveAndCloseAsync();
}
public async void Approve_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@@ -1,63 +1,19 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Threading;
using LibationUiBase.Forms;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginCallback : ILoginCallback
{
private Account _account { get; }
public string DeviceName { get; } = "Libation";
public AvaloniaLoginCallback(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public async Task<string> Get2faCodeAsync(string prompt)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new _2faCodeDialog(prompt);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return dialog.Code;
return null;
});
public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new CaptchaDialog(password, captchaImage);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (dialog.Password, dialog.Answer);
return (null, null);
});
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new MfaDialog(mfaConfig);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (dialog.SelectedName, dialog.SelectedValue);
return (null, null);
});
public async Task<(string email, string password)> GetLoginAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new LoginCallbackDialog(_account);
if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (_account.AccountId, dialog.Password);
return (null, null);
});
public async Task ShowApprovalNeededAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new ApprovalNeededDialog();
await dialog.ShowDialogAsync();
});
public Task<string> Get2faCodeAsync(string prompt) => throw new System.NotSupportedException();
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> throw new System.NotSupportedException();
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> throw new System.NotSupportedException();
public Task<(string email, string password)> GetLoginAsync()
=> throw new System.NotSupportedException();
public Task ShowApprovalNeededAsync() => throw new System.NotSupportedException();
}
}

View File

@@ -1,5 +1,6 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Threading;
using LibationFileManager;
using LibationUiBase.Forms;
@@ -11,14 +12,13 @@ namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : ILoginChoiceEager
{
public ILoginCallback LoginCallback { get; }
public ILoginCallback LoginCallback { get; } = new AvaloniaLoginCallback();
private readonly Account _account;
public AvaloniaLoginChoiceEager(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new AvaloniaLoginCallback(_account);
}
public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
@@ -26,41 +26,84 @@ namespace LibationAvalonia.Dialogs.Login
private async Task<ChoiceOut?> StartAsyncInternal(ChoiceIn choiceIn)
{
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
try
{
try
{
var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl);
if (await weblogin.ShowDialogAsync(App.MainWindow) is DialogResult.OK)
return ChoiceOut.External(weblogin.ResponseUrl);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}");
}
if (await BrowserLoginAsync(choiceIn.LoginUrl) is ChoiceOut external)
return external;
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to use the {nameof(NativeWebDialog)}");
}
var dialog = new LoginChoiceEagerDialog(_account);
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return await externalDialog.ShowDialogAsync() is DialogResult.OK
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
if (await dialog.ShowDialogAsync() is not DialogResult.OK ||
(dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
return null;
private async Task<ChoiceOut?> BrowserLoginAsync(string url)
{
TaskCompletionSource<ChoiceOut?> tcs = new();
switch (dialog.LoginMethod)
NativeWebDialog dialog = new()
{
case LoginMethod.Api:
return ChoiceOut.WithApi(dialog.Account.AccountId, dialog.Password);
case LoginMethod.External:
{
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return await externalDialog.ShowDialogAsync() is DialogResult.OK
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
Title = "Audible Login",
CanUserResize = true,
Source = new Uri(url)
};
dialog.AdapterCreated += Dialog_AdapterCreated;
dialog.NavigationCompleted += Dialog_NavigationCompleted;
dialog.Closing += (_, _) => tcs.TrySetResult(null);
dialog.NavigationStarted += (_, e) =>
{
if (e.Request?.AbsolutePath.StartsWith("/ap/maplanding") is true)
{
tcs.TrySetResult(ChoiceOut.External(e.Request.ToString()));
dialog.Close();
}
};
if (!Configuration.IsLinux && App.MainWindow is TopLevel topLevel)
dialog.Show(topLevel);
else
dialog.Show();
return await tcs.Task;
}
private async void Dialog_NavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e)
{
if (e.IsSuccess && sender is NativeWebDialog dialog)
{
await dialog.InvokeScript(getScript(_account.AccountId));
}
}
private void Dialog_AdapterCreated(object? sender, WebViewAdapterEventArgs e)
{
if ((sender as NativeWebDialog)?.TryGetWindow() is Window window)
{
window.Width = 450;
window.Height = 700;
}
}
private static string getScript(string accountID) => $$"""
(function() {
function populateForm(){
var email = document.querySelector("input[name='email']");
if (email !== null)
email.value = '{{accountID}}';
var pass = document.querySelector("input[name='password']");
if (pass !== null)
pass.focus();
}
window.addEventListener("load", (event) => { populateForm(); });
populateForm();
})()
""";
}
}

View File

@@ -1,72 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="220" d:DesignHeight="250"
MinWidth="220" MinHeight="250"
MaxWidth="220" MaxHeight="250"
Width="220" Height="250"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA">
<Grid
RowDefinitions="Auto,Auto,Auto,Auto,*"
ColumnDefinitions="Auto,*"
Margin="10">
<Panel
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
MinWidth="200"
MinHeight="70"
Background="LightGray">
<Image
Stretch="None"
Source="{Binding CaptchaImage}" />
</Panel>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
VerticalAlignment="Center"
Text="Password:" />
<TextBox
Name="passwordBox"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0,10,0,0"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,10,10,0"
VerticalAlignment="Center"
Text="CAPTCHA&#xa;answer:" />
<TextBox
Name="captchaBox"
Grid.Row="3"
Grid.Column="1"
Margin="0,10,0,0"
Text="{Binding Answer, Mode=TwoWay}" />
<Button
Grid.Row="4"
Grid.Column="1"
Padding="0,5,0,5"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -1,120 +0,0 @@
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using LibationAvalonia.ViewModels;
using ReactiveUI;
using System.IO;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class CaptchaDialog : DialogWindow
{
public string Password => _viewModel.Password;
public string Answer => _viewModel.Answer;
private readonly CaptchaDialogViewModel _viewModel;
public CaptchaDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
}
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
//Avalonia doesn't support animated gifs.
//Deconstruct gifs into frames and manually switch them.
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
var gifFrames = new Bitmap[gif.Frames.Count];
var frameDelayMs = new int[gif.Frames.Count];
for (int i = 0; i < gif.Frames.Count; i++)
{
var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
using var clonedFrame = gif.Frames.CloneFrame(i);
using var framems = new MemoryStream();
clonedFrame.Save(framems, gifEncoder);
framems.Position = 0;
gifFrames[i] = new Bitmap(framems);
frameDelayMs[i] = frameMetadata.FrameDelay * 10;
}
DataContext = _viewModel = new(password, gifFrames, frameDelayMs);
Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
}
protected override async Task SaveAndCloseAsync()
{
if (string.IsNullOrWhiteSpace(_viewModel.Password))
{
await MessageBox.Show(this, "Please re-enter your password");
return;
}
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { _viewModel.Answer });
await _viewModel.StopAsync();
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)
=> await SaveAndCloseAsync();
}
public class CaptchaDialogViewModel : ViewModelBase
{
public string Answer { get; set; }
public string Password { get; set; }
public Bitmap CaptchaImage { get => _captchaImage; private set => this.RaiseAndSetIfChanged(ref _captchaImage, value); }
private Bitmap _captchaImage;
private bool keepSwitching = true;
private readonly Task FrameSwitch;
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames, int[] frameDelayMs)
{
Password = password;
if (gifFrames.Length == 1)
{
FrameSwitch = Task.CompletedTask;
CaptchaImage = gifFrames[0];
}
else
{
FrameSwitch = SwitchFramesAsync(gifFrames, frameDelayMs);
}
}
public async Task StopAsync()
{
keepSwitching = false;
await FrameSwitch;
}
private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
{
int index = 0;
while (keepSwitching)
{
CaptchaImage = gifFrames[index];
await Task.Delay(frameDelayMs[index++]);
index %= gifFrames.Length;
}
foreach (var frame in gifFrames)
frame.Dispose();
}
}
}

View File

@@ -1,38 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="120"
MinWidth="300" MinHeight="120"
Width="300" Height="120"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.LoginCallbackDialog"
Title="Audible Login">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid Margin="0,5,0,5" Grid.Row="2" Grid.Column="0" ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="Password: " />
<TextBox Grid.Column="1" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
</Grid>
<Button
Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Padding="30,5,30,5"
Content="Submit"
Click="Submit_Click"/>
</Grid>
</Window>

View File

@@ -1,42 +0,0 @@
using AudibleUtilities;
using Avalonia.Controls;
using Dinah.Core;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class LoginCallbackDialog : DialogWindow
{
public Account Account { get; }
public string Password { get; set; }
public LoginCallbackDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
DataContext = this;
}
}
public LoginCallbackDialog(Account account) : this()
{
Account = account;
DataContext = this;
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Account?.AccountId?.ToMask(), passwordLength = Password?.Length });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@@ -1,70 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="360" d:DesignHeight="200"
MinWidth="370" MinHeight="200"
Width="370" Height="200"
WindowStartupLocation="CenterOwner"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Dialogs.Login.LoginChoiceEagerDialog"
Title="Audible Login">
<Grid RowDefinitions="Auto,Auto,Auto,*" ColumnDefinitions="*" Margin="5">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBlock Text="Locale: " />
<TextBlock Text="{Binding Account.Locale.Name}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<TextBlock Text="Username: " />
<TextBlock Text="{Binding Account.AccountId}" />
</StackPanel>
<Grid
Grid.Row="2"
Grid.Column="0"
Margin="0,5,0,5"
ColumnDefinitions="Auto,*,Auto">
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="Password: " />
<TextBox
Grid.Column="1"
PasswordChar="*"
Text="{Binding Password, Mode=TwoWay}" />
<Button
Margin="5,0"
Grid.Column="2"
VerticalAlignment="Stretch"
Content="Submit"
Command="{Binding SaveAndCloseAsync}" />
</Grid>
<StackPanel
Grid.Row="3"
VerticalAlignment="Bottom">
<controls:LinkLabel
Tapped="ExternalLoginLink_Tapped"
Text="Trouble logging in? Click here to log in with your browser." />
<TextBlock
TextWrapping="Wrap"
Text="This more advanced login is recommended if you're experiencing errors logging in the conventional way above or if you're not comfortable typing your password here." />
</StackPanel>
</Grid>
</Window>

View File

@@ -1,50 +0,0 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Controls;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class LoginChoiceEagerDialog : DialogWindow
{
public Account Account { get; }
public string Password { get; set; }
public LoginMethod LoginMethod { get; private set; }
public LoginChoiceEagerDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
if (Design.IsDesignMode)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
Account = accounts.FirstOrDefault();
DataContext = this;
}
}
public LoginChoiceEagerDialog(Account account) : this()
{
Account = account;
DataContext = this;
}
protected override async Task SaveAndCloseAsync()
{
if (LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
{
await MessageBox.Show(this, "Please enter your password");
return;
}
await base.SaveAndCloseAsync();
}
public async void ExternalLoginLink_Tapped(object sender, Avalonia.Input.TappedEventArgs e)
{
LoginMethod = LoginMethod.External;
await SaveAndCloseAsync();
}
}
}

View File

@@ -1,19 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="200"
MinWidth="400" MinHeight="200"
MaxWidth="400" MaxHeight="400"
Width="400" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
Title="Two-Step Verification">
<Grid RowDefinitions="*,Auto">
<StackPanel Grid.Row="0" Margin="10,0,10,10" Name="rbStackPanel" Orientation="Vertical"/>
<Button Grid.Row="1" Content="Submit" Margin="10" Padding="30,5,30,5" Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -1,137 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using LibationUiBase.Forms;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class MfaDialog : DialogWindow
{
public string SelectedName { get; private set; }
public string SelectedValue { get; private set; }
private RbValues Values { get; } = new();
public MfaDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
if (Design.IsDesignMode)
{
var mfaConfig = new AudibleApi.MfaConfig { Title = "My title" };
mfaConfig.Buttons.Add(new() { Text = "Enter the OTP from the authenticator app", Name = "otpDeviceContext", Value = "aAbBcC=, TOTP" });
mfaConfig.Buttons.Add(new() { Text = "Send an SMS to my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, SMS" });
mfaConfig.Buttons.Add(new() { Text = "Call me on my number ending with 123", Name = "otpDeviceContext", Value = "dDeEfE=, VOICE" });
loadRadioButtons(mfaConfig);
}
}
public MfaDialog(AudibleApi.MfaConfig mfaConfig) : this()
{
loadRadioButtons(mfaConfig);
}
private void loadRadioButtons(AudibleApi.MfaConfig mfaConfig)
{
if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
Title = mfaConfig.Title;
rbStackPanel = this.Find<StackPanel>(nameof(rbStackPanel));
foreach (var conf in mfaConfig.Buttons)
{
var rb = new RbValue(conf);
Values.AddButton(rb);
RadioButton radioButton = new()
{
Content = new TextBlock { Text = conf.Text },
Margin = new Thickness(0, 10, 0, 0),
};
radioButton.Bind(
RadioButton.IsCheckedProperty,
new Binding
{
Source = rb,
Path = nameof(rb.IsChecked)
});
rbStackPanel.Children.Add(radioButton);
}
}
protected override async Task SaveAndCloseAsync()
{
var selected = Values.CheckedButton;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new
{
text = selected?.Text,
name = selected?.Name,
value = selected?.Value
});
if (selected is null)
{
await MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SelectedName = selected.Name;
SelectedValue = selected.Value;
await base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
private class RbValue : ViewModels.ViewModelBase
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => this.RaiseAndSetIfChanged(ref _isChecked, value);
}
public AudibleApi.MfaConfigButton MfaConfigButton { get; }
public RbValue(AudibleApi.MfaConfigButton mfaConfig)
{
MfaConfigButton = mfaConfig;
}
}
private class RbValues
{
private List<RbValue> ButtonValues { get; } = new();
public AudibleApi.MfaConfigButton CheckedButton => ButtonValues.SingleOrDefault(rb => rb.IsChecked)?.MfaConfigButton;
public void AddButton(RbValue rbValue)
{
if (ButtonValues.Contains(rbValue))
return;
rbValue.PropertyChanged += RbValue_PropertyChanged;
rbValue.IsChecked = ButtonValues.Count == 0;
ButtonValues.Add(rbValue);
}
private void RbValue_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var button = sender as RbValue;
if (button.IsChecked)
{
foreach (var rb in ButtonValues.Where(rb => rb != button))
rb.IsChecked = false;
}
}
}
}
}

View File

@@ -1,12 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:Class="LibationAvalonia.Dialogs.Login.WebLoginDialog"
Width="500" Height="800"
WindowStartupLocation="CenterOwner"
Title="Audible Login">
<controls:NativeWebView Name="webView" />
</Window>

View File

@@ -1,55 +0,0 @@
using Avalonia.Controls;
using Dinah.Core;
using LibationUiBase.Forms;
using System;
namespace LibationAvalonia.Dialogs.Login
{
public partial class WebLoginDialog : Window
{
public string ResponseUrl { get; private set; }
private readonly string accountID;
public WebLoginDialog()
{
InitializeComponent();
webView.NavigationStarted += WebView_NavigationStarted;
webView.DOMContentLoaded += WebView_NavigationCompleted;
}
public WebLoginDialog(string accountID, string loginUrl) : this()
{
this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID));
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
}
private void WebView_NavigationStarted(object sender, LibationFileManager.WebViewNavigationEventArgs e)
{
if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
{
ResponseUrl = e.Request.ToString();
Close(DialogResult.OK);
}
}
private async void WebView_NavigationCompleted(object sender, EventArgs e)
{
await webView.InvokeScriptAsync(getScript(accountID));
}
private static string getScript(string accountID) => $$"""
(function() {
var inputs = document.getElementsByTagName('input');
for (index = 0; index < inputs.length; ++index) {
if (inputs[index].name.includes('email')) {
inputs[index].value = '{{accountID}}';
}
if (inputs[index].name.includes('password')) {
inputs[index].focus();
}
}
})()
""";
}
}

View File

@@ -1,44 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200"
MinWidth="200" MinHeight="200"
MaxWidth="200" MaxHeight="200"
Width="200" Height="200"
WindowStartupLocation="CenterOwner"
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
Title="2FA Code">
<Grid
VerticalAlignment="Stretch"
ColumnDefinitions="*" Margin="5"
RowDefinitions="*,Auto,Auto,Auto">
<TextBlock
TextAlignment="Center"
TextWrapping="Wrap"
Text="{Binding Prompt}" />
<TextBlock
Margin="5"
Grid.Row="1"
TextAlignment="Center"
Text="Enter 2FA Code" />
<TextBox
Name="_2FABox"
Margin="5,0,5,0"
Grid.Row="2"
HorizontalContentAlignment="Center"
Text="{Binding Code, Mode=TwoWay}" />
<Button
Margin="5"
Grid.Row="3"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -1,35 +0,0 @@
using Avalonia.Controls;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class _2faCodeDialog : DialogWindow
{
public string Code { get; set; }
public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
public _2faCodeDialog() : base(saveAndRestorePosition: false)
{
InitializeComponent();
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
}
public _2faCodeDialog(string prompt) : this()
{
Prompt = prompt;
DataContext = this;
Opened += (_, _) => _2FABox.Focus();
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

View File

@@ -3,8 +3,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<TargetFramework>net9.0</TargetFramework>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationIcon>Assets/libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
@@ -79,6 +78,7 @@
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" />
<PackageReference Include="WebViewControlAvaloniaFree" Version="11.3.14" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,10 +7,6 @@ namespace LibationFileManager
{
public interface IInteropFunctions
{
/// <summary>
/// Implementation of native web view control https://github.com/maxkatz6/AvaloniaWebView
/// </summary>
IWebViewAdapter? CreateWebViewAdapter();
void SetFolderIcon(string image, string directory);
void DeleteFolderIcon(string directory);
Process RunAsRoot(string exe, string args);
@@ -19,39 +15,4 @@ namespace LibationFileManager
string ReleaseIdString { get; }
}
public class WebViewNavigationEventArgs : EventArgs
{
public Uri? Request { get; init; }
}
public interface IWebView
{
event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
event EventHandler? DOMContentLoaded;
bool CanGoBack { get; }
bool CanGoForward { get; }
Uri? Source { get; set; }
bool GoBack();
bool GoForward();
Task<string?> InvokeScriptAsync(string scriptName);
void Navigate(Uri url);
Task NavigateToString(string text);
void Refresh();
void Stop();
}
public interface IWebViewAdapter : IWebView
{
object NativeWebView { get; }
IPlatformHandle2 PlatformHandle { get; }
void HandleResize(int width, int height, float zoom);
bool HandleKeyDown(uint key, uint keyModifiers);
}
public interface IPlatformHandle2
{
IntPtr Handle { get; }
string? HandleDescriptor { get; }
}
}

View File

@@ -10,7 +10,6 @@ namespace LibationFileManager
public NullInteropFunctions() { }
public NullInteropFunctions(params object[] values) { }
public IWebViewAdapter? CreateWebViewAdapter() => throw new PlatformNotSupportedException();
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
public bool CanUpgrade => throw new PlatformNotSupportedException();

View File

@@ -1,83 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class ApprovalNeededDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.approvedBtn = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// approvedBtn
//
this.approvedBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.approvedBtn.Location = new System.Drawing.Point(18, 75);
this.approvedBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.approvedBtn.Name = "approvedBtn";
this.approvedBtn.Size = new System.Drawing.Size(92, 27);
this.approvedBtn.TabIndex = 1;
this.approvedBtn.Text = "Approved";
this.approvedBtn.UseVisualStyleBackColor = true;
this.approvedBtn.Click += new System.EventHandler(this.approvedBtn_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(14, 10);
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(314, 45);
this.label1.TabIndex = 0;
this.label1.Text = "Amazon is sending you an email.\r\n\r\nPlease press this button after you approve the" +
" notification.";
//
// ApprovalNeededDialog
//
this.AcceptButton = this.approvedBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(345, 115);
this.Controls.Add(this.label1);
this.Controls.Add(this.approvedBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ApprovalNeededDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Approval Alert Detected";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button approvedBtn;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class ApprovalNeededDialog : Form
{
public ApprovalNeededDialog()
{
InitializeComponent();
}
private void approvedBtn_Click(object sender, EventArgs e)
{
Serilog.Log.Logger.Information("Submit button clicked");
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,131 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class CaptchaDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
captchaPb = new System.Windows.Forms.PictureBox();
answerTb = new System.Windows.Forms.TextBox();
submitBtn = new System.Windows.Forms.Button();
answerLbl = new System.Windows.Forms.Label();
label1 = new System.Windows.Forms.Label();
passwordTb = new System.Windows.Forms.TextBox();
((System.ComponentModel.ISupportInitialize)captchaPb).BeginInit();
SuspendLayout();
//
// captchaPb
//
captchaPb.Location = new System.Drawing.Point(13, 14);
captchaPb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
captchaPb.Name = "captchaPb";
captchaPb.Size = new System.Drawing.Size(235, 81);
captchaPb.TabIndex = 0;
captchaPb.TabStop = false;
//
// answerTb
//
answerTb.Location = new System.Drawing.Point(136, 130);
answerTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
answerTb.Name = "answerTb";
answerTb.Size = new System.Drawing.Size(111, 23);
answerTb.TabIndex = 2;
//
// submitBtn
//
submitBtn.Location = new System.Drawing.Point(159, 171);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(88, 27);
submitBtn.TabIndex = 2;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// answerLbl
//
answerLbl.AutoSize = true;
answerLbl.Location = new System.Drawing.Point(13, 133);
answerLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
answerLbl.Name = "answerLbl";
answerLbl.Size = new System.Drawing.Size(106, 15);
answerLbl.TabIndex = 0;
answerLbl.Text = "CAPTCHA answer: ";
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(13, 104);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(60, 15);
label1.TabIndex = 0;
label1.Text = "Password:";
//
// passwordTb
//
passwordTb.Location = new System.Drawing.Point(81, 101);
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
passwordTb.Name = "passwordTb";
passwordTb.PasswordChar = '*';
passwordTb.Size = new System.Drawing.Size(167, 23);
passwordTb.TabIndex = 1;
//
// CaptchaDialog
//
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(261, 210);
Controls.Add(passwordTb);
Controls.Add(label1);
Controls.Add(answerLbl);
Controls.Add(submitBtn);
Controls.Add(answerTb);
Controls.Add(captchaPb);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "CaptchaDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "CAPTCHA";
((System.ComponentModel.ISupportInitialize)captchaPb).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.PictureBox captchaPb;
private System.Windows.Forms.TextBox answerTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label answerLbl;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox passwordTb;
}
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Linq;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class CaptchaDialog : Form
{
public string Answer { get; private set; }
public string Password { get; private set; }
private MemoryStream ms { get; }
private Image image { get; }
public CaptchaDialog() => InitializeComponent();
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
this.FormClosed += (_, __) => { ms?.Dispose(); image?.Dispose(); };
ms = new MemoryStream(captchaImage);
image = Image.FromStream(ms);
this.captchaPb.Image = image;
passwordTb.Text = password;
(string.IsNullOrEmpty(password) ? passwordTb : answerTb).Select();
}
private void submitBtn_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(passwordTb.Text))
{
MessageBox.Show(this, "Please re-enter your password");
return;
}
Answer = answerTb.Text;
Password = passwordTb.Text;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,121 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class LoginCallbackDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.passwordLbl = new System.Windows.Forms.Label();
this.passwordTb = new System.Windows.Forms.TextBox();
this.submitBtn = new System.Windows.Forms.Button();
this.localeLbl = new System.Windows.Forms.Label();
this.usernameLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// passwordLbl
//
this.passwordLbl.AutoSize = true;
this.passwordLbl.Location = new System.Drawing.Point(14, 47);
this.passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.passwordLbl.Name = "passwordLbl";
this.passwordLbl.Size = new System.Drawing.Size(57, 15);
this.passwordLbl.TabIndex = 2;
this.passwordLbl.Text = "Password";
//
// passwordTb
//
this.passwordTb.Location = new System.Drawing.Point(83, 44);
this.passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.passwordTb.Name = "passwordTb";
this.passwordTb.PasswordChar = '*';
this.passwordTb.Size = new System.Drawing.Size(233, 23);
this.passwordTb.TabIndex = 3;
//
// submitBtn
//
this.submitBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.submitBtn.Location = new System.Drawing.Point(229, 74);
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.submitBtn.Name = "submitBtn";
this.submitBtn.Size = new System.Drawing.Size(88, 27);
this.submitBtn.TabIndex = 4;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
//
// localeLbl
//
this.localeLbl.AutoSize = true;
this.localeLbl.Location = new System.Drawing.Point(14, 10);
this.localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.localeLbl.Name = "localeLbl";
this.localeLbl.Size = new System.Drawing.Size(61, 15);
this.localeLbl.TabIndex = 0;
this.localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
this.usernameLbl.AutoSize = true;
this.usernameLbl.Location = new System.Drawing.Point(14, 25);
this.usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.usernameLbl.Name = "usernameLbl";
this.usernameLbl.Size = new System.Drawing.Size(80, 15);
this.usernameLbl.TabIndex = 1;
this.usernameLbl.Text = "Username: {0}";
//
// LoginCallbackDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(330, 114);
this.Controls.Add(this.usernameLbl);
this.Controls.Add(this.localeLbl);
this.Controls.Add(this.submitBtn);
this.Controls.Add(this.passwordLbl);
this.Controls.Add(this.passwordTb);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "LoginCallbackDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Audible Login";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label passwordLbl;
private System.Windows.Forms.TextBox passwordTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
}
}

View File

@@ -1,37 +0,0 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
namespace LibationWinForms.Dialogs.Login
{
public partial class LoginCallbackDialog : Form
{
private string accountId { get; }
public string Email { get; private set; }
public string Password { get; private set; }
public LoginCallbackDialog(Account account)
{
InitializeComponent();
accountId = account.AccountId;
// do not allow user to change login id here. if they do then jsonpath will fail
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
}
private void submitBtn_Click(object sender, EventArgs e)
{
Email = accountId;
Password = this.passwordTb.Text;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Email?.ToMask(), passwordLength = Password.Length });
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,60 +0,0 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,158 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class LoginChoiceEagerDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
passwordLbl = new System.Windows.Forms.Label();
passwordTb = new System.Windows.Forms.TextBox();
submitBtn = new System.Windows.Forms.Button();
localeLbl = new System.Windows.Forms.Label();
usernameLbl = new System.Windows.Forms.Label();
externalLoginLink = new System.Windows.Forms.LinkLabel();
externalLoginLbl2 = new System.Windows.Forms.Label();
externalLoginLbl1 = new System.Windows.Forms.Label();
SuspendLayout();
//
// passwordLbl
//
passwordLbl.AutoSize = true;
passwordLbl.Location = new System.Drawing.Point(14, 47);
passwordLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
passwordLbl.Name = "passwordLbl";
passwordLbl.Size = new System.Drawing.Size(57, 15);
passwordLbl.TabIndex = 2;
passwordLbl.Text = "Password";
//
// passwordTb
//
passwordTb.Location = new System.Drawing.Point(83, 44);
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
passwordTb.Name = "passwordTb";
passwordTb.PasswordChar = '*';
passwordTb.Size = new System.Drawing.Size(233, 23);
passwordTb.TabIndex = 3;
//
// submitBtn
//
submitBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
submitBtn.Location = new System.Drawing.Point(293, 176);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(88, 27);
submitBtn.TabIndex = 7;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// localeLbl
//
localeLbl.AutoSize = true;
localeLbl.Location = new System.Drawing.Point(14, 10);
localeLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
localeLbl.Name = "localeLbl";
localeLbl.Size = new System.Drawing.Size(61, 15);
localeLbl.TabIndex = 0;
localeLbl.Text = "Locale: {0}";
//
// usernameLbl
//
usernameLbl.AutoSize = true;
usernameLbl.Location = new System.Drawing.Point(14, 25);
usernameLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
usernameLbl.Name = "usernameLbl";
usernameLbl.Size = new System.Drawing.Size(80, 15);
usernameLbl.TabIndex = 1;
usernameLbl.Text = "Username: {0}";
//
// externalLoginLink
//
externalLoginLink.AutoSize = true;
externalLoginLink.Location = new System.Drawing.Point(14, 93);
externalLoginLink.Name = "externalLoginLink";
externalLoginLink.Size = new System.Drawing.Size(166, 15);
externalLoginLink.TabIndex = 4;
externalLoginLink.TabStop = true;
externalLoginLink.Text = "Trouble Logging in? Click here";
externalLoginLink.LinkClicked += externalLoginLink_LinkClicked;
//
// externalLoginLbl2
//
externalLoginLbl2.AutoSize = true;
externalLoginLbl2.Location = new System.Drawing.Point(14, 108);
externalLoginLbl2.Name = "externalLoginLbl2";
externalLoginLbl2.Size = new System.Drawing.Size(352, 45);
externalLoginLbl2.TabIndex = 6;
externalLoginLbl2.Text = "This more advanced login is recommended if you're experiencing\r\nerrors logging in the conventional way above or if you're not\r\ncomfortable typing your password here.";
//
// externalLoginLbl1
//
externalLoginLbl1.AutoSize = true;
externalLoginLbl1.Location = new System.Drawing.Point(177, 93);
externalLoginLbl1.Name = "externalLoginLbl1";
externalLoginLbl1.Size = new System.Drawing.Size(158, 15);
externalLoginLbl1.TabIndex = 5;
externalLoginLbl1.Text = "to log in using your browser.";
//
// LoginChoiceEagerDialog
//
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(394, 216);
Controls.Add(externalLoginLbl2);
Controls.Add(externalLoginLbl1);
Controls.Add(externalLoginLink);
Controls.Add(usernameLbl);
Controls.Add(localeLbl);
Controls.Add(submitBtn);
Controls.Add(passwordLbl);
Controls.Add(passwordTb);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "LoginChoiceEagerDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Audible Login";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label passwordLbl;
private System.Windows.Forms.TextBox passwordTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label localeLbl;
private System.Windows.Forms.Label usernameLbl;
private System.Windows.Forms.LinkLabel externalLoginLink;
private System.Windows.Forms.Label externalLoginLbl2;
private System.Windows.Forms.Label externalLoginLbl1;
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Windows.Forms;
using AudibleUtilities;
using Dinah.Core;
namespace LibationWinForms.Dialogs.Login
{
public partial class LoginChoiceEagerDialog : Form
{
private string accountId { get; }
public AudibleApi.LoginMethod LoginMethod { get; private set; }
public string Email { get; private set; }
public string Password { get; private set; }
public LoginChoiceEagerDialog(Account account)
{
InitializeComponent();
accountId = account.AccountId;
// do not allow user to change login id here. if they do then jsonpath will fail
this.localeLbl.Text = string.Format(this.localeLbl.Text, account.Locale.Name);
this.usernameLbl.Text = string.Format(this.usernameLbl.Text, accountId);
}
private void externalLoginLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
LoginMethod = AudibleApi.LoginMethod.External;
DialogResult = DialogResult.OK;
this.Close();
}
private void submitBtn_Click(object sender, EventArgs e)
{
Email = accountId;
Password = this.passwordTb.Text;
if (LoginMethod is AudibleApi.LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
{
MessageBox.Show("Please enter your password");
return;
}
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { email = Email?.ToMask(), passwordLength = Password.Length });
LoginMethod = AudibleApi.LoginMethod.Api;
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,60 +0,0 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,113 +0,0 @@

namespace LibationWinForms.Dialogs.Login
{
partial class MfaDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.submitBtn = new System.Windows.Forms.Button();
this.radioButton1 = new System.Windows.Forms.RadioButton();
this.radioButton2 = new System.Windows.Forms.RadioButton();
this.radioButton3 = new System.Windows.Forms.RadioButton();
this.SuspendLayout();
//
// submitBtn
//
this.submitBtn.Location = new System.Drawing.Point(14, 93);
this.submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.submitBtn.Name = "submitBtn";
this.submitBtn.Size = new System.Drawing.Size(88, 27);
this.submitBtn.TabIndex = 3;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
//
// radioButton1
//
this.radioButton1.AutoSize = true;
this.radioButton1.Checked = true;
this.radioButton1.Location = new System.Drawing.Point(14, 14);
this.radioButton1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.radioButton1.Name = "radioButton1";
this.radioButton1.Size = new System.Drawing.Size(242, 19);
this.radioButton1.TabIndex = 0;
this.radioButton1.TabStop = true;
this.radioButton1.Text = "Enter the OTP from the authenticator app";
this.radioButton1.UseVisualStyleBackColor = true;
//
// radioButton2
//
this.radioButton2.AutoSize = true;
this.radioButton2.Location = new System.Drawing.Point(14, 40);
this.radioButton2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.radioButton2.Name = "radioButton2";
this.radioButton2.Size = new System.Drawing.Size(172, 19);
this.radioButton2.TabIndex = 1;
this.radioButton2.Text = "Send an SMS to my number";
this.radioButton2.UseVisualStyleBackColor = true;
//
// radioButton3
//
this.radioButton3.AutoSize = true;
this.radioButton3.Location = new System.Drawing.Point(14, 67);
this.radioButton3.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.radioButton3.Name = "radioButton3";
this.radioButton3.Size = new System.Drawing.Size(147, 19);
this.radioButton3.TabIndex = 2;
this.radioButton3.Text = "Call me on my number";
this.radioButton3.UseVisualStyleBackColor = true;
//
// MfaDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(398, 129);
this.Controls.Add(this.radioButton3);
this.Controls.Add(this.radioButton2);
this.Controls.Add(this.radioButton1);
this.Controls.Add(this.submitBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "MfaDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Two-step verification";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.RadioButton radioButton1;
private System.Windows.Forms.RadioButton radioButton2;
private System.Windows.Forms.RadioButton radioButton3;
}
}

View File

@@ -1,92 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class MfaDialog : Form
{
private RadioButton[] radioButtons { get; }
private AudibleApi.MfaConfig _mfaConfig { get; }
public MfaDialog(AudibleApi.MfaConfig mfaConfig)
{
InitializeComponent();
_mfaConfig = mfaConfig;
radioButtons = new[] { this.radioButton1, this.radioButton2, this.radioButton3 };
// optional string settings
if (!string.IsNullOrWhiteSpace(mfaConfig.Title))
this.Text = mfaConfig.Title;
setRadioButton(0, this.radioButton1);
setRadioButton(1, this.radioButton2);
setRadioButton(2, this.radioButton3);
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
paramButtonCount = mfaConfig.Buttons.Count,
visibleRadioButtonCount = radioButtons.Count(rb => rb.Visible)
});
}
private void setRadioButton(int pos, RadioButton rb)
{
if (_mfaConfig.Buttons.Count <= pos)
{
rb.Checked = false;
rb.Enabled = false;
rb.Visible = false;
return;
}
var btn = _mfaConfig.Buttons[pos];
// optional
if (!string.IsNullOrWhiteSpace(btn.Text))
rb.Text = btn.Text;
// mandatory values
rb.Name = btn.Name;
rb.Tag = btn.Value;
}
public string SelectedName { get; private set; }
public string SelectedValue { get; private set; }
private void submitBtn_Click(object sender, EventArgs e)
{
var selected = radioButtons.FirstOrDefault(rb => rb.Checked);
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new
{
rb1_visible = radioButton1.Visible,
rb1_checked = radioButton1.Checked,
rb2_visible = radioButton2.Visible,
rb2_checked = radioButton2.Checked,
rb3_visible = radioButton3.Visible,
rb3_checked = radioButton3.Checked,
isSelected = selected is not null,
name = selected?.Name,
value = selected?.Tag
});
if (selected is null)
{
MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
SelectedName = selected.Name;
SelectedValue = (string)selected.Tag;
DialogResult = DialogResult.OK;
// Close() not needed for AcceptButton
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,5 +1,5 @@
using Dinah.Core;
using LibationFileManager;
using Microsoft.Web.WebView2.WinForms;
using System;
using System.Windows.Forms;
@@ -9,19 +9,19 @@ namespace LibationWinForms.Login
{
public string ResponseUrl { get; private set; }
private readonly string accountID;
private readonly IWebViewAdapter webView;
private readonly WebView2 webView;
public WebLoginDialog()
{
InitializeComponent();
webView = InteropFactory.Create().CreateWebViewAdapter();
webView = new WebView2();
var webViewControl = webView.NativeWebView as Control;
webViewControl.Dock = DockStyle.Fill;
Controls.Add(webViewControl);
webView.Dock = DockStyle.Fill;
Controls.Add(webView);
webView.NavigationStarted += WebView_NavigationStarted;
webView.DOMContentLoaded += WebView_DOMContentLoaded;
webView.NavigationStarting += WebView_NavigationStarting;
webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
this.SetLibationIcon();
}
public WebLoginDialog(string accountID, string loginUrl) : this()
@@ -30,32 +30,36 @@ namespace LibationWinForms.Login
webView.Source = new Uri(ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)));
}
private void WebView_NavigationStarted(object sender, WebViewNavigationEventArgs e)
private void WebView_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
{
if (e.Request?.AbsolutePath.Contains("/ap/maplanding") is true)
if (e.Uri.Contains("/ap/maplanding") is true)
{
ResponseUrl = e.Request.ToString();
ResponseUrl = e.Uri;
DialogResult = DialogResult.OK;
Close();
}
}
private async void WebView_DOMContentLoaded(object sender, EventArgs e)
private void WebView_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
await webView.InvokeScriptAsync(getScript(accountID));
webView.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
}
private async void CoreWebView2_DOMContentLoaded(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e)
{
await webView.ExecuteScriptAsync(getScript(accountID));
}
private static string getScript(string accountID) => $$"""
(function() {
var inputs = document.getElementsByTagName('input');
for (index = 0; index < inputs.length; ++index) {
if (inputs[index].name.includes('email')) {
inputs[index].value = '{{accountID}}';
}
if (inputs[index].name.includes('password')) {
inputs[index].focus();
}
}
var email = document.querySelector("input[name='email']");
if (email !== null)
email.value = '{{accountID}}';
var pass = document.querySelector("input[name='password']");
if (pass !== null)
pass.focus();
})()
""";
}

View File

@@ -1,23 +0,0 @@
using System;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public abstract class WinformLoginBase
{
protected Control Owner { get; }
protected WinformLoginBase(Control owner)
{
Owner = owner;
}
/// <returns>True if ShowDialog's DialogResult == OK</returns>
protected bool ShowDialog(Form dialog)
=> Owner.Invoke(() =>
{
var result = dialog.ShowDialog(Owner);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
});
}
}

View File

@@ -7,59 +7,17 @@ using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login
{
public class WinformLoginCallback : WinformLoginBase, ILoginCallback
public class WinformLoginCallback : ILoginCallback
{
private Account _account { get; }
public string DeviceName { get; } = "Libation";
public WinformLoginCallback(Account account, Control owner) : base(owner)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public Task<string> Get2faCodeAsync(string prompt)
=> Owner.Invoke(() =>
{
using var dialog = new _2faCodeDialog(prompt);
if (ShowDialog(dialog))
return Task.FromResult(dialog.Code);
return Task.FromResult<string>(null);
});
public Task<string> Get2faCodeAsync(string prompt) => throw new System.NotSupportedException();
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> Owner.Invoke(() =>
{
using var dialog = new CaptchaDialog(password, captchaImage);
if (ShowDialog(dialog))
return Task.FromResult((dialog.Password, dialog.Answer));
return Task.FromResult<(string, string)>((null, null));
});
=> throw new System.NotSupportedException();
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> Owner.Invoke(() =>
{
using var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog))
return Task.FromResult((dialog.SelectedName, dialog.SelectedValue));
return Task.FromResult<(string, string)>((null, null));
});
=> throw new System.NotSupportedException();
public Task<(string email, string password)> GetLoginAsync()
=> Owner.Invoke(() =>
{
using var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog))
return Task.FromResult((dialog.Email, dialog.Password));
return Task.FromResult<(string, string)>((null, null));
});
public Task ShowApprovalNeededAsync()
=> Owner.Invoke(() =>
{
using var dialog = new ApprovalNeededDialog();
ShowDialog(dialog);
return Task.CompletedTask;
});
=> throw new System.NotSupportedException();
public Task ShowApprovalNeededAsync() => throw new System.NotSupportedException();
}
}

View File

@@ -7,16 +7,16 @@ using LibationWinForms.Dialogs.Login;
namespace LibationWinForms.Login
{
public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager
public class WinformLoginChoiceEager : ILoginChoiceEager
{
public ILoginCallback LoginCallback { get; private set; }
public ILoginCallback LoginCallback { get; } = new WinformLoginCallback();
private Account _account { get; }
public WinformLoginChoiceEager(Account account, Control owner) : base(owner)
private Control Owner { get; }
public WinformLoginChoiceEager(Account account, Control owner)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new WinformLoginCallback(_account, owner);
Owner = Dinah.Core.ArgumentValidator.EnsureNotNull(owner, nameof(owner));
}
public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
@@ -38,26 +38,20 @@ namespace LibationWinForms.Login
}
}
using var dialog = new LoginChoiceEagerDialog(_account);
if (!ShowDialog(dialog) || (dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password)))
return null;
switch (dialog.LoginMethod)
{
case LoginMethod.Api:
return Task.FromResult(ChoiceOut.WithApi(dialog.Email, dialog.Password));
case LoginMethod.External:
{
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return Task.FromResult(
ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null);
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
}
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return Task.FromResult(
ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null);
}
/// <returns>True if ShowDialog's DialogResult == OK</returns>
private bool ShowDialog(Form dialog)
=> Owner.Invoke(() =>
{
var result = dialog.ShowDialog(Owner);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
});
}
}

View File

@@ -1,104 +0,0 @@
namespace LibationWinForms.Dialogs.Login
{
partial class _2faCodeDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
submitBtn = new System.Windows.Forms.Button();
codeTb = new System.Windows.Forms.TextBox();
label1 = new System.Windows.Forms.Label();
promptLbl = new System.Windows.Forms.Label();
SuspendLayout();
//
// submitBtn
//
submitBtn.Location = new System.Drawing.Point(18, 108);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(191, 27);
submitBtn.TabIndex = 1;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// codeTb
//
codeTb.Location = new System.Drawing.Point(108, 79);
codeTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
codeTb.Name = "codeTb";
codeTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
codeTb.Size = new System.Drawing.Size(101, 23);
codeTb.TabIndex = 0;
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(13, 82);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(87, 15);
label1.TabIndex = 2;
label1.Text = "Enter 2FA Code";
//
// promptLbl
//
promptLbl.Location = new System.Drawing.Point(13, 9);
promptLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
promptLbl.Name = "promptLbl";
promptLbl.Size = new System.Drawing.Size(196, 59);
promptLbl.TabIndex = 2;
promptLbl.Text = "[Prompt]";
//
// _2faCodeDialog
//
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
ClientSize = new System.Drawing.Size(222, 147);
Controls.Add(promptLbl);
Controls.Add(label1);
Controls.Add(codeTb);
Controls.Add(submitBtn);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "_2faCodeDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "2FA Code";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.TextBox codeTb;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label promptLbl;
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.Dialogs.Login
{
public partial class _2faCodeDialog : Form
{
public string Code { get; private set; }
public _2faCodeDialog() => InitializeComponent();
public _2faCodeDialog(string prompt) : this()
{
promptLbl.Text = prompt;
}
private void submitBtn_Click(object sender, EventArgs e)
{
Code = this.codeTb.Text.Trim();
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Code });
DialogResult = DialogResult.OK;
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -5,12 +5,10 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<IsPublishable>true</IsPublishable>
@@ -43,6 +41,7 @@
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="9.0.3.1" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,6 +1,6 @@
[Desktop Entry]
Name=Libation
Exec=/usr/bin/libation
Exec=sh -c 'export WEBKIT_DISABLE_COMPOSITING_MODE=1; libation'
Icon=libation
Comment=Liberate your Audiobooks
Terminal=false

View File

@@ -21,7 +21,6 @@ namespace LinuxConfigApp
public LinuxInterop() { }
public LinuxInterop(params object[] values) { }
public IWebViewAdapter CreateWebViewAdapter() => null;
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();

View File

@@ -10,7 +10,6 @@ namespace MacOSConfigApp
public MacOSInterop() { }
public MacOSInterop(params object[] values) { }
public IWebViewAdapter CreateWebViewAdapter() => null;
public void SetFolderIcon(string image, string directory)
{
Process.Start("fileicon", $"set {directory.SurroundWithQuotes()} {image.SurroundWithQuotes()}").WaitForExit();

View File

@@ -1,134 +0,0 @@
/* Work-in-progress
*
*
using LibationFileManager;
using ObjCRuntime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebKit;
#nullable enable
namespace MacOSConfigApp;
internal class WKNavigationDelegate1 : WKNavigationDelegate
{
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
{
base.DidStartProvisionalNavigation(webView, navigation);
}
}
internal class MacWebViewAdapter : IWebViewAdapter, IDisposable
{
private readonly WKWebView _webView;
public IPlatformHandle2 PlatformHandle { get; }
public bool CanGoBack => _webView.CanGoBack;
public bool CanGoForward => _webView.CanGoForward;
public Uri? Source { get => _webView?.Url; set => throw new NotImplementedException(); }
public object NativeWebView { get; }
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
public event EventHandler? DOMContentLoaded;
WKNavigationDelegate1 navDelegate;
public MacWebViewAdapter()
{
var frame = new CGRect(0, 0, 500, 800);
NativeWebView = _webView = new WKWebView(frame, new WKWebViewConfiguration());
_webView.NavigationDelegate = navDelegate = new WKNavigationDelegate1();
PlatformHandle = new MacViewHandle(_webView.Handle);
}
public void Dispose()
{
_webView?.Dispose();
}
public bool GoBack()
{
if (_webView.CanGoBack)
{
_webView.GoBack();
return true;
}
else return false;
}
public bool GoForward()
{
if (_webView.CanGoForward)
{
_webView.GoForward();
return true;
}
else return false;
}
public bool HandleKeyDown(uint key, uint keyModifiers)
{
return false;
}
public void HandleResize(int width, int height, float zoom)
{
}
public async Task<string?> InvokeScriptAsync(string scriptName)
{
var result = await _webView.EvaluateJavaScriptAsync(scriptName);
return result.ToString();
}
public void Navigate(Uri url)
{
NSUrl? nsurl = url;
if (nsurl is null) return;
var request = new NSUrlRequest(nsurl);
_webView.LoadRequest(request);
}
public Task NavigateToString(string text)
{
throw new NotImplementedException();
}
public void Refresh()
{
throw new NotImplementedException();
}
public void Stop()
{
throw new NotImplementedException();
}
}
internal class MacViewHandle : IPlatformHandle2
{
private NativeHandle? _view;
public MacViewHandle(NativeHandle view)
{
_view = view;
}
public nint Handle => _view?.Handle ?? 0;
public string HandleDescriptor => "NativeHandle";
}
*/

View File

@@ -11,10 +11,6 @@ namespace WindowsConfigApp
{
public WinInterop() { }
public WinInterop(params object[] values) { }
#nullable enable
public IWebViewAdapter? CreateWebViewAdapter() => new WindowsWebView2Adapter();
#nullable disable
public void SetFolderIcon(string image, string directory)
{
var icon = Image.Load(image).ToIcon();

View File

@@ -3,7 +3,6 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
@@ -25,10 +24,6 @@
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\LibationUiBase\LibationUiBase.csproj" />
</ItemGroup>

View File

@@ -1,114 +0,0 @@
using LibationFileManager;
using Microsoft.Web.WebView2.WinForms;
using System;
using System.Threading.Tasks;
#nullable enable
namespace WindowsConfigApp;
internal class WindowsWebView2Adapter : IWebViewAdapter, IDisposable
{
public object NativeWebView { get; }
private readonly WebView2 _webView;
public WindowsWebView2Adapter()
{
NativeWebView = _webView = new WebView2();
PlatformHandle = new WebView2Handle { Handle = _webView.Handle };
_webView.CoreWebView2InitializationCompleted += _webView_CoreWebView2InitializationCompleted;
_webView.NavigationStarting += (s, a) =>
{
NavigationStarted?.Invoke(this, new WebViewNavigationEventArgs { Request = new Uri(a.Uri) });
};
_webView.NavigationCompleted += (s, a) =>
{
NavigationCompleted?.Invoke(this, new WebViewNavigationEventArgs { Request = _webView.Source });
};
}
private void _webView_CoreWebView2InitializationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e)
{
_webView.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
_webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
}
private void CoreWebView2_DOMContentLoaded(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e)
=> DOMContentLoaded?.Invoke(this, e);
public IPlatformHandle2 PlatformHandle { get; }
public bool CanGoBack => _webView.CanGoBack;
public bool CanGoForward => _webView.CanGoForward;
public Uri? Source
{
get => _webView.Source;
set => _webView.Source = value;
}
public event EventHandler<WebViewNavigationEventArgs>? NavigationStarted;
public event EventHandler<WebViewNavigationEventArgs>? NavigationCompleted;
public event EventHandler? DOMContentLoaded;
public void Dispose()
{
_webView.Dispose();
}
public bool GoBack()
{
_webView.GoBack();
return true;
}
public bool GoForward()
{
_webView.GoForward();
return true;
}
public async Task<string?> InvokeScriptAsync(string scriptName)
{
return await _webView.ExecuteScriptAsync(scriptName);
}
public void Navigate(Uri url)
{
_webView.Source = url;
}
public async Task NavigateToString(string text)
{
await _webView.EnsureCoreWebView2Async();
_webView.NavigateToString(text);
}
public void Refresh()
{
_webView.Refresh();
}
public void Stop()
{
_webView.Stop();
}
public void HandleResize(int width, int height, float zoom)
{
}
public bool HandleKeyDown(uint key, uint keyModifiers)
{
return false;
}
}
internal class WebView2Handle : IPlatformHandle2
{
public IntPtr Handle { get; init; }
public string HandleDescriptor => "HWND";
}