diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index caa91e33..605016a1 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -69,7 +69,7 @@ jobs: LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj ` --configuration ${{ env.DOTNET_CONFIGURATION }} ` --output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} ` - -p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml + -p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml dotnet publish ` LibationCli/LibationCli.csproj ` --configuration ${{ env.DOTNET_CONFIGURATION }} ` diff --git a/Source/LibationAvalonia/Controls/NativeWebView.cs b/Source/LibationAvalonia/Controls/NativeWebView.cs new file mode 100644 index 00000000..0aad165d --- /dev/null +++ b/Source/LibationAvalonia/Controls/NativeWebView.cs @@ -0,0 +1,176 @@ +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? NavigationCompleted; + + public event EventHandler? 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 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(); + } + } +} diff --git a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml index 10cde367..1a6476f2 100644 --- a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml @@ -6,6 +6,7 @@ MinWidth="240" MinHeight="140" MaxWidth="240" MaxHeight="140" Width="240" Height="140" + WindowStartupLocation="CenterOwner" x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog" Title="Approval Alert Detected" Icon="/Assets/libation.ico"> diff --git a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs index c1f3a4b7..5bab6bb3 100644 --- a/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/Login/ApprovalNeededDialog.axaml.cs @@ -4,7 +4,7 @@ namespace LibationAvalonia.Dialogs.Login { public partial class ApprovalNeededDialog : DialogWindow { - public ApprovalNeededDialog() + public ApprovalNeededDialog() : base(saveAndRestorePosition: false) { InitializeComponent(); } diff --git a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs index 143b31ca..f90fe55d 100644 --- a/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs +++ b/Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginChoiceEager.cs @@ -1,5 +1,7 @@ using AudibleApi; using AudibleUtilities; +using Avalonia.Threading; +using LibationFileManager; using System; using System.Threading.Tasks; @@ -23,6 +25,20 @@ namespace LibationAvalonia.Dialogs.Login public async Task StartAsync(ChoiceIn choiceIn) { + if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10) + { + try + { + var weblogin = new WebLoginDialog(_account.AccountId, choiceIn.LoginUrl); + if (await weblogin.ShowDialog(App.MainWindow) is DialogResult.OK) + return ChoiceOut.External(weblogin.ResponseUrl); + } + catch (Exception ex) + { + Serilog.Log.Logger.Error(ex, $"Failed to run {nameof(WebLoginDialog)}"); + } + } + var dialog = new LoginChoiceEagerDialog(_account); if (await dialog.ShowDialogAsync() is not DialogResult.OK || diff --git a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml index 363d0f8c..1ab14620 100644 --- a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml @@ -6,6 +6,7 @@ MinWidth="220" MinHeight="250" MaxWidth="220" MaxHeight="250" Width="220" Height="250" + WindowStartupLocation="CenterOwner" x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog" Title="CAPTCHA" Icon="/Assets/libation.ico"> diff --git a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs index df681ae3..c079332b 100644 --- a/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/Login/CaptchaDialog.axaml.cs @@ -13,7 +13,7 @@ namespace LibationAvalonia.Dialogs.Login public string Answer => _viewModel.Answer; private readonly CaptchaDialogViewModel _viewModel; - public CaptchaDialog() + public CaptchaDialog() : base(saveAndRestorePosition: false) { InitializeComponent(); passwordBox = this.FindControl(nameof(passwordBox)); diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml index 0a27e987..7531be98 100644 --- a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml @@ -5,6 +5,7 @@ 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" Icon="/Assets/libation.ico"> diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs index 35ed88b5..faf919fc 100644 --- a/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/Login/LoginCallbackDialog.axaml.cs @@ -11,7 +11,7 @@ namespace LibationAvalonia.Dialogs.Login public Account Account { get; } public string Password { get; set; } - public LoginCallbackDialog() + public LoginCallbackDialog() : base(saveAndRestorePosition: false) { InitializeComponent(); diff --git a/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml b/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml index e84e40c4..cc9c3448 100644 --- a/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/Login/LoginChoiceEagerDialog.axaml @@ -35,7 +35,7 @@ Grid.Row="2" Grid.Column="0" Margin="0,5,0,5" - ColumnDefinitions="Auto,*"> + ColumnDefinitions="Auto,*,Auto"> +