diff --git a/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.Designer.cs b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.Designer.cs new file mode 100644 index 00000000..dc75e648 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.Designer.cs @@ -0,0 +1,46 @@ +namespace LibationWinForms.Login +{ + partial class WebLoginDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + SuspendLayout(); + // + // WebLoginDialog + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(484, 761); + Name = "WebLoginDialog"; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Audible Login"; + ResumeLayout(false); + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs new file mode 100644 index 00000000..75abedd4 --- /dev/null +++ b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.cs @@ -0,0 +1,67 @@ +using Dinah.Core; +using Microsoft.Web.WebView2.WinForms; +using System; +using System.Windows.Forms; + +namespace LibationWinForms.Login +{ + public partial class WebLoginDialog : Form + { + public string ResponseUrl { get; private set; } + private readonly string loginUrl; + private readonly string accountID; + private readonly WebView2 webView = new(); + public WebLoginDialog() + { + InitializeComponent(); + webView.Dock = DockStyle.Fill; + Controls.Add(webView); + Shown += WebLoginDialog_Shown; + this.SetLibationIcon(); + } + + public WebLoginDialog(string accountID, string loginUrl) : this() + { + this.accountID = ArgumentValidator.EnsureNotNullOrWhiteSpace(accountID, nameof(accountID)); + this.loginUrl = ArgumentValidator.EnsureNotNullOrWhiteSpace(loginUrl, nameof(loginUrl)); + } + + private async void WebLoginDialog_Shown(object sender, EventArgs e) + { + await webView.EnsureCoreWebView2Async(); + webView.CoreWebView2.NavigationStarting += CoreWebView2_NavigationStarting; + webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded; + webView.CoreWebView2.Navigate(loginUrl); + } + + private async void CoreWebView2_DOMContentLoaded(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e) + { + await webView.CoreWebView2.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(); + } + } + })() + """; + + private void CoreWebView2_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e) + { + if (new Uri(e.Uri).AbsolutePath.Contains("/ap/maplanding")) + { + ResponseUrl = e.Uri; + e.Cancel = true; + DialogResult = DialogResult.OK; + Close(); + } + } + } +} diff --git a/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.resx b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.resx new file mode 100644 index 00000000..f298a7be --- /dev/null +++ b/Source/LibationWinForms/Dialogs/Login/WebLoginDialog.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Source/LibationWinForms/Dialogs/Login/WinformLoginBase.cs b/Source/LibationWinForms/Dialogs/Login/WinformLoginBase.cs index 1de76464..790df589 100644 --- a/Source/LibationWinForms/Dialogs/Login/WinformLoginBase.cs +++ b/Source/LibationWinForms/Dialogs/Login/WinformLoginBase.cs @@ -1,15 +1,22 @@ using System; +using System.Windows.Forms; namespace LibationWinForms.Dialogs.Login { public abstract class WinformLoginBase { - /// True if ShowDialog's DialogResult == OK - protected static bool ShowDialog(System.Windows.Forms.Form dialog) + private readonly IWin32Window _owner; + protected WinformLoginBase(IWin32Window owner) { - var result = dialog.ShowDialog(); + _owner = owner; + } + + /// True if ShowDialog's DialogResult == OK + protected bool ShowDialog(Form dialog) + { + var result = dialog.ShowDialog(_owner); Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result }); - return result == System.Windows.Forms.DialogResult.OK; + return result == DialogResult.OK; } } } diff --git a/Source/LibationWinForms/Dialogs/Login/WinformLoginCallback.cs b/Source/LibationWinForms/Dialogs/Login/WinformLoginCallback.cs index fd3e0e83..92c36c03 100644 --- a/Source/LibationWinForms/Dialogs/Login/WinformLoginCallback.cs +++ b/Source/LibationWinForms/Dialogs/Login/WinformLoginCallback.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using System.Windows.Forms; using AudibleApi; using AudibleUtilities; using LibationWinForms.Dialogs.Login; @@ -12,7 +13,7 @@ namespace LibationWinForms.Login public string DeviceName { get; } = "Libation"; - public WinformLoginCallback(Account account) + public WinformLoginCallback(Account account, IWin32Window owner) : base(owner) { _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account)); } diff --git a/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs b/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs index 062893dd..96c8425e 100644 --- a/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs +++ b/Source/LibationWinForms/Dialogs/Login/WinformLoginChoiceEager.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using System.Windows.Forms; using AudibleApi; using AudibleUtilities; using LibationWinForms.Dialogs.Login; @@ -9,20 +10,31 @@ namespace LibationWinForms.Login public class WinformLoginChoiceEager : WinformLoginBase, ILoginChoiceEager { /// Convenience method. Recommended when wiring up Winforms to - public static async Task ApiExtendedFunc(Account account) => await ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account)); + public static Func> CreateApiExtendedFunc(IWin32Window owner) => a => ApiExtendedFunc(a, owner); + + private static async Task ApiExtendedFunc(Account account, IWin32Window owner) + => await ApiExtended.CreateAsync(account, new WinformLoginChoiceEager(account, owner)); public ILoginCallback LoginCallback { get; private set; } private Account _account { get; } - public WinformLoginChoiceEager(Account account) + private WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner) { _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account)); - LoginCallback = new WinformLoginCallback(_account); + LoginCallback = new WinformLoginCallback(_account, owner); } public Task StartAsync(ChoiceIn choiceIn) { + if (Environment.OSVersion.Version.Major >= 10) + { + using var browserLogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl); + + if (ShowDialog(browserLogin)) + return Task.FromResult(ChoiceOut.External(browserLogin.ResponseUrl)); + } + using var dialog = new LoginChoiceEagerDialog(_account); if (!ShowDialog(dialog) || (dialog.LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(dialog.Password))) @@ -33,13 +45,13 @@ namespace LibationWinForms.Login case LoginMethod.Api: return Task.FromResult(ChoiceOut.WithApi(dialog.Email, dialog.Password)); case LoginMethod.External: - { - using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl); + { + 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"); } diff --git a/Source/LibationWinForms/Form1.ScanAuto.cs b/Source/LibationWinForms/Form1.ScanAuto.cs index ca91c770..6b671c6a 100644 --- a/Source/LibationWinForms/Form1.ScanAuto.cs +++ b/Source/LibationWinForms/Form1.ScanAuto.cs @@ -32,7 +32,7 @@ namespace LibationWinForms // in autoScan, new books SHALL NOT show dialog try { - Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); + Task importAsync() => LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts); if (InvokeRequired) await Invoke(importAsync); else diff --git a/Source/LibationWinForms/Form1.ScanManual.cs b/Source/LibationWinForms/Form1.ScanManual.cs index d745c6d2..83f5149f 100644 --- a/Source/LibationWinForms/Form1.ScanManual.cs +++ b/Source/LibationWinForms/Form1.ScanManual.cs @@ -74,7 +74,7 @@ namespace LibationWinForms { try { - var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts); + var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), accounts); // this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop if (Configuration.Instance.ShowImportedStats && newAdded > 0) diff --git a/Source/LibationWinForms/GridView/ProductsDisplay.cs b/Source/LibationWinForms/GridView/ProductsDisplay.cs index 6196b6a6..95c1ea7f 100644 --- a/Source/LibationWinForms/GridView/ProductsDisplay.cs +++ b/Source/LibationWinForms/GridView/ProductsDisplay.cs @@ -279,7 +279,7 @@ namespace LibationWinForms.GridView .Select(lbe => lbe.LibraryBook) .Where(lb => !lb.Book.HasLiberated()); - var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts); + var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.CreateApiExtendedFunc(this), lib, accounts); var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList(); diff --git a/Source/LibationWinForms/LibationWinForms.csproj b/Source/LibationWinForms/LibationWinForms.csproj index c482fe34..b051ac6e 100644 --- a/Source/LibationWinForms/LibationWinForms.csproj +++ b/Source/LibationWinForms/LibationWinForms.csproj @@ -38,6 +38,7 @@ +