Add legacy auth method to GUI, remove unnecessary activity log entries, and avoid deleting the auth.json file when auth fails

This commit is contained in:
whimsical-c4lic0 2026-02-20 00:35:19 -06:00
parent f536a34772
commit 0a709f97ea
6 changed files with 195 additions and 114 deletions

View File

@ -209,14 +209,6 @@ public class Program(IServiceProvider serviceProvider)
Log.Error("Auth failed");
authService.CurrentAuth = null;
if (!configService.CurrentConfig.DisableBrowserAuth)
{
if (File.Exists("auth.json"))
{
File.Delete("auth.json");
}
}
if (!configService.CurrentConfig.NonInteractiveMode &&
!configService.CurrentConfig.DisableBrowserAuth)
{
@ -747,11 +739,6 @@ public class Program(IServiceProvider serviceProvider)
else if (File.Exists("auth.json"))
{
Log.Information("Auth file found but could not be deserialized");
if (!configService.CurrentConfig.DisableBrowserAuth)
{
Log.Debug("Deleting auth.json");
File.Delete("auth.json");
}
if (configService.CurrentConfig.NonInteractiveMode)
{

View File

@ -6,6 +6,10 @@ public static class Constants
public const string DocumentationUrl = "https://docs.ofdl.tools/";
public const string AuthHelperExtensionUrl = "https://github.com/whimsical-c4lic0/OF-DL-Auth-Helper/";
public const string LegacyAuthDocumentationUrl = "https://docs.ofdl.tools/config/auth/#legacy-methods";
public const string ApiUrl = "https://onlyfans.com/api2/v2";
public const int ApiPageSize = 50;

View File

@ -5,6 +5,7 @@ public enum AppScreen
Loading,
Config,
Auth,
ManualAuth,
UserSelection,
Error
}

View File

@ -12,6 +12,7 @@ using Avalonia.Threading;
using Newtonsoft.Json;
using OF_DL.Enumerations;
using OF_DL.Gui.Services;
using OF_DL.Helpers;
using OF_DL.Models;
using OF_DL.Models.Config;
using OF_DL.Models.Downloads;
@ -258,7 +259,18 @@ public partial class MainWindowViewModel(
[ObservableProperty] private string _authScreenMessage = string.Empty;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartBrowserLoginCommand))]
[ObservableProperty] private string _manualAuthScreenMessage = string.Empty;
[ObservableProperty] private string _manualAuthInstructionsText = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GoBackToAuthScreenCommand))]
[NotifyCanExecuteChangedFor(nameof(ContinueWithManualAuthCommand))]
private bool _isManualAuthValidationInProgress;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartBrowserLoginCommand))]
[NotifyCanExecuteChangedFor(nameof(OpenManualAuthScreenCommand))]
private bool _isBrowserLoginInProgress;
[ObservableProperty] private string _errorMessage = string.Empty;
@ -354,6 +366,8 @@ public partial class MainWindowViewModel(
public bool IsAuthScreen => CurrentScreen == AppScreen.Auth;
public bool IsManualAuthScreen => CurrentScreen == AppScreen.ManualAuth;
public bool IsUserSelectionScreen => CurrentScreen == AppScreen.UserSelection;
public bool IsErrorScreen => CurrentScreen == AppScreen.Error;
@ -545,10 +559,12 @@ public partial class MainWindowViewModel(
AvailableUsers.Clear();
UserLists.Clear();
SelectedListName = null;
IsManualAuthValidationInProgress = false;
ManualAuthScreenMessage = string.Empty;
AuthenticatedUserDisplay = "Not authenticated.";
AuthScreenMessage = "You have been logged out. Click 'Login with Browser' to authenticate.";
StatusMessage = "Logged out.";
AuthScreenMessage =
"You have been logged out. OF DL needs access to your OnlyFans account.\nAn included web browser can be used to sign-in, or you can create an \"auth.json\" file manually.";
CurrentScreen = AppScreen.Auth;
OnPropertyChanged(nameof(SelectedUsersSummary));
DownloadSelectedCommand.NotifyCanExecuteChanged();
@ -570,7 +586,6 @@ public partial class MainWindowViewModel(
EnforceGuiOnlyConfigValues(configService.CurrentConfig);
BuildConfigFields(configService.CurrentConfig);
ConfigScreenMessage = "Edit configuration values and save to apply changes.";
StatusMessage = "Editing configuration.";
CurrentScreen = AppScreen.Config;
}
@ -584,7 +599,6 @@ public partial class MainWindowViewModel(
if (!loaded)
{
ConfigScreenMessage = "Configuration is still invalid.";
StatusMessage = ConfigScreenMessage;
CurrentScreen = AppScreen.Config;
return;
}
@ -592,7 +606,6 @@ public partial class MainWindowViewModel(
if (_configReturnScreen == AppScreen.UserSelection && _allUsers.Count > 0)
{
CurrentScreen = AppScreen.UserSelection;
StatusMessage = "Configuration changes canceled.";
return;
}
@ -611,7 +624,6 @@ public partial class MainWindowViewModel(
}
ConfigScreenMessage = "Refreshing user lists...";
StatusMessage = ConfigScreenMessage;
try
{
@ -624,14 +636,11 @@ public partial class MainWindowViewModel(
UpdateIgnoredUsersListFieldOptions();
ConfigScreenMessage = $"User lists refreshed ({_allLists.Count}).";
StatusMessage = ConfigScreenMessage;
AppendLog(ConfigScreenMessage);
}
catch (Exception ex)
{
ConfigScreenMessage = $"Could not refresh user lists: {ex.Message}";
StatusMessage = ConfigScreenMessage;
AppendLog(ConfigScreenMessage);
AppendLog($"Could not refresh user lists: {ex.Message}");
}
}
@ -641,7 +650,6 @@ public partial class MainWindowViewModel(
if (!TryBuildConfig(out Config newConfig))
{
ConfigScreenMessage = "Fix configuration validation errors and save again.";
StatusMessage = ConfigScreenMessage;
return;
}
@ -655,14 +663,12 @@ public partial class MainWindowViewModel(
{
ConfigScreenMessage = "config.conf could not be loaded after saving. Please review your values.";
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
BuildConfigFields(configService.CurrentConfig);
return;
}
BuildConfigFields(configService.CurrentConfig);
ConfigScreenMessage = "Configuration saved.";
StatusMessage = "Configuration saved.";
if (!await ValidateEnvironmentAsync(false))
{
@ -682,16 +688,8 @@ public partial class MainWindowViewModel(
[RelayCommand(CanExecute = nameof(CanStartBrowserLogin))]
private async Task StartBrowserLoginAsync()
{
if (configService.CurrentConfig.DisableBrowserAuth)
{
AuthScreenMessage = "Browser authentication is disabled in config.";
return;
}
IsBrowserLoginInProgress = true;
AppendLog("Starting browser authentication flow.");
StartBrowserLoginCommand.NotifyCanExecuteChanged();
bool success;
try
{
@ -702,8 +700,7 @@ public partial class MainWindowViewModel(
return;
}
AuthScreenMessage = message;
AppendLog(message);
Dispatcher.UIThread.Post(() => SetLoading(message));
});
}
finally
@ -714,27 +711,86 @@ public partial class MainWindowViewModel(
if (!success || authService.CurrentAuth == null)
{
AuthScreenMessage =
"Authentication failed. Log in using the opened browser window and retry.";
"Authentication failed. Retry the browser based login, or use\n'Manual Authentication' to continue.";
CurrentScreen = AppScreen.Auth;
StatusMessage = "Authentication failed.";
AppendLog("Browser authentication failed.");
return;
}
await authService.SaveToFileAsync();
bool isAuthValid = await ValidateCurrentAuthAsync(true);
bool isAuthValid = await ValidateCurrentAuthAsync();
if (!isAuthValid)
{
AuthScreenMessage = "Authentication is still invalid after login. Please retry.";
AuthScreenMessage =
"Authentication is still invalid after login.\nPlease retry, or use 'Manual Authentication' to continue.";
CurrentScreen = AppScreen.Auth;
StatusMessage = "Authentication failed.";
return;
}
await LoadUsersAndListsAsync();
}
[RelayCommand(CanExecute = nameof(CanOpenManualAuthScreen))]
private void OpenManualAuthScreen()
{
IsManualAuthValidationInProgress = false;
ManualAuthScreenMessage = string.Empty;
string configFolder = EnvironmentHelper.IsRunningInDocker()
? "your OF DL config folder"
: "the same folder as OF DL";
ManualAuthInstructionsText =
$"An \"auth.json\" file is required to use OF DL. See the documentation for creating this file manually." +
$"\nOnce you've created the \"auth.json\", save it to {configFolder}.";
CurrentScreen = AppScreen.ManualAuth;
}
[RelayCommand(CanExecute = nameof(CanGoBackToAuthScreen))]
private void GoBackToAuthScreen()
{
IsManualAuthValidationInProgress = false;
ManualAuthScreenMessage = string.Empty;
AuthScreenMessage =
"OF DL needs access to your OnlyFans account.\nAn included web browser can be used to sign-in, or you can create an \"auth.json\" file manually.";
CurrentScreen = AppScreen.Auth;
}
[RelayCommand(CanExecute = nameof(CanContinueWithManualAuth))]
private async Task ContinueWithManualAuthAsync()
{
IsManualAuthValidationInProgress = true;
ManualAuthScreenMessage = "Validating auth.json...";
try
{
bool loadedFromFile = await authService.LoadFromFileAsync();
if (!loadedFromFile)
{
authService.CurrentAuth = null;
IsAuthenticated = false;
AuthenticatedUserDisplay = "Not authenticated.";
ManualAuthScreenMessage =
"auth.json was not found or could not be read. Update auth.json and try again.";
return;
}
bool isValid = await ValidateCurrentAuthAsync();
if (!isValid)
{
ManualAuthScreenMessage =
"auth.json failed validation. Update the file and try again.";
return;
}
await LoadUsersAndListsAsync();
}
finally
{
IsManualAuthValidationInProgress = false;
}
}
[RelayCommand(CanExecute = nameof(CanApplySelectedList))]
private async Task SelectUsersFromListAsync()
{
@ -763,7 +819,6 @@ public partial class MainWindowViewModel(
user.IsSelected = selectedUsernames.Contains(user.Username);
}
StatusMessage = $"Selected {selectedUsernames.Count} users from list '{SelectedListName}'.";
OnPropertyChanged(nameof(SelectedUsersSummary));
OnPropertyChanged(nameof(AllUsersSelected));
DownloadSelectedCommand.NotifyCanExecuteChanged();
@ -831,7 +886,6 @@ public partial class MainWindowViewModel(
if (_workCancellationSource is { IsCancellationRequested: false })
{
_workCancellationSource.Cancel();
StatusMessage = "Stop requested. Waiting for current operation to cancel...";
UpdateProgressStatus("Stopping...");
AppendLog("Stop requested.");
}
@ -842,13 +896,11 @@ public partial class MainWindowViewModel(
List<SelectableUserViewModel> selectedUsers = AvailableUsers.Where(user => user.IsSelected).ToList();
if (!downloadPurchasedTabOnly && selectedUsers.Count == 0)
{
StatusMessage = "Select at least one user before downloading.";
return;
}
if (downloadPurchasedTabOnly && _allUsers.Count == 0)
{
StatusMessage = "No users are loaded. Refresh users and retry.";
return;
}
@ -868,9 +920,6 @@ public partial class MainWindowViewModel(
AppendLog(downloadPurchasedTabOnly
? "Starting Purchased Tab download."
: $"Starting download for {selectedUsers.Count} users.");
StatusMessage = downloadPurchasedTabOnly
? "Starting Purchased Tab download..."
: $"Starting download for {selectedUsers.Count} users...";
// Show progress bar immediately with indeterminate state
StartDownloadProgress(
@ -915,19 +964,14 @@ public partial class MainWindowViewModel(
ThrowIfStopRequested();
eventHandler.OnScrapeComplete(DateTime.Now - start);
StatusMessage = downloadPurchasedTabOnly
? "Purchased Tab download completed."
: "Download run completed.";
}
catch (OperationCanceledException)
{
StatusMessage = "Operation canceled.";
AppendLog("Operation canceled.");
}
catch (Exception ex)
{
AppendLog($"Download failed: {ex.Message}");
StatusMessage = "Download failed. Check logs.";
}
finally
{
@ -990,11 +1034,27 @@ public partial class MainWindowViewModel(
!IsDownloading &&
!IsBrowserLoginInProgress;
private bool CanOpenManualAuthScreen() =>
CurrentScreen == AppScreen.Auth &&
!IsDownloading &&
!IsBrowserLoginInProgress;
private bool CanGoBackToAuthScreen() =>
CurrentScreen == AppScreen.ManualAuth &&
!IsDownloading &&
!IsManualAuthValidationInProgress;
private bool CanContinueWithManualAuth() =>
CurrentScreen == AppScreen.ManualAuth &&
!IsDownloading &&
!IsManualAuthValidationInProgress;
partial void OnCurrentScreenChanged(AppScreen value)
{
OnPropertyChanged(nameof(IsLoadingScreen));
OnPropertyChanged(nameof(IsConfigScreen));
OnPropertyChanged(nameof(IsAuthScreen));
OnPropertyChanged(nameof(IsManualAuthScreen));
OnPropertyChanged(nameof(IsUserSelectionScreen));
OnPropertyChanged(nameof(IsErrorScreen));
OnPropertyChanged(nameof(SelectedUsersSummary));
@ -1007,6 +1067,9 @@ public partial class MainWindowViewModel(
LogoutCommand.NotifyCanExecuteChanged();
EditConfigCommand.NotifyCanExecuteChanged();
StartBrowserLoginCommand.NotifyCanExecuteChanged();
OpenManualAuthScreenCommand.NotifyCanExecuteChanged();
GoBackToAuthScreenCommand.NotifyCanExecuteChanged();
ContinueWithManualAuthCommand.NotifyCanExecuteChanged();
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
}
@ -1021,6 +1084,9 @@ public partial class MainWindowViewModel(
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
LogoutCommand.NotifyCanExecuteChanged();
StartBrowserLoginCommand.NotifyCanExecuteChanged();
OpenManualAuthScreenCommand.NotifyCanExecuteChanged();
GoBackToAuthScreenCommand.NotifyCanExecuteChanged();
ContinueWithManualAuthCommand.NotifyCanExecuteChanged();
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
}
@ -1093,7 +1159,6 @@ public partial class MainWindowViewModel(
ConfigScreenMessage =
"config.conf is invalid. Update all fields below and save to continue.";
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return;
}
@ -1114,7 +1179,6 @@ public partial class MainWindowViewModel(
{
if (_allUsers.Count == 0)
{
StatusMessage = "No users are loaded. Refresh users and retry.";
return;
}
@ -1135,7 +1199,6 @@ public partial class MainWindowViewModel(
DateTime start = DateTime.Now;
string label = request.Type == SingleDownloadType.Post ? "single post" : "single paid message";
AppendLog($"Starting {label} download from URL.");
StatusMessage = $"Starting {label} download...";
StartDownloadProgress($"Initializing {label} download...", 0, false);
@ -1155,9 +1218,8 @@ public partial class MainWindowViewModel(
{
if (!_allUsers.TryGetValue(request.Username, out long userId))
{
StatusMessage =
$"Creator '{request.Username}' is not in loaded users. Refresh users and ensure you are subscribed.";
AppendLog(StatusMessage);
AppendLog(
$"Creator '{request.Username}' is not in loaded users. Refresh users and ensure you are subscribed.");
return;
}
@ -1179,8 +1241,7 @@ public partial class MainWindowViewModel(
string? resolvedUsername = await downloadOrchestrationService.ResolveUsernameAsync(userId);
if (string.IsNullOrWhiteSpace(resolvedUsername))
{
StatusMessage = $"Could not resolve username for user ID {userId}.";
AppendLog(StatusMessage);
AppendLog($"Could not resolve username for user ID {userId}.");
return;
}
@ -1205,19 +1266,14 @@ public partial class MainWindowViewModel(
ThrowIfStopRequested();
eventHandler.OnScrapeComplete(DateTime.Now - start);
StatusMessage = request.Type == SingleDownloadType.Post
? "Single post download completed."
: "Single paid message download completed.";
}
catch (OperationCanceledException)
{
StatusMessage = "Operation canceled.";
AppendLog("Operation canceled.");
}
catch (Exception ex)
{
AppendLog($"Single item download failed: {ex.Message}");
StatusMessage = "Single item download failed. Check logs.";
}
finally
{
@ -1275,7 +1331,6 @@ public partial class MainWindowViewModel(
if (!confirmed)
{
StatusMessage = "Download canceled.";
AppendLog("Download canceled after missing CDM keys warning.");
}
@ -1354,17 +1409,11 @@ public partial class MainWindowViewModel(
bool hasValidAuth = await TryLoadAndValidateExistingAuthAsync(logAuthenticationMessage);
if (!hasValidAuth)
{
if (configService.CurrentConfig.DisableBrowserAuth)
{
ShowError(
"Authentication is missing or invalid and browser auth is disabled. Enable browser auth in config or provide a valid auth.json.");
return;
}
IsManualAuthValidationInProgress = false;
ManualAuthScreenMessage = string.Empty;
AuthScreenMessage =
"Authentication is required. Click 'Login with Browser' and complete the OnlyFans login flow.";
"OF DL needs access to your OnlyFans account.\nAn included web browser can be used to sign-in, or you can create an \"auth.json\" file manually.";
CurrentScreen = AppScreen.Auth;
StatusMessage = "Authentication required.";
return;
}
@ -1401,7 +1450,6 @@ public partial class MainWindowViewModel(
configService.CurrentConfig.FFmpegPath,
"FFmpeg");
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return false;
}
@ -1418,7 +1466,6 @@ public partial class MainWindowViewModel(
configService.CurrentConfig.FFprobePath,
"FFprobe");
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return false;
}
@ -1442,14 +1489,13 @@ public partial class MainWindowViewModel(
private async Task<bool> TryLoadAndValidateExistingAuthAsync(bool logAuthenticationMessage)
{
bool loadedFromFile = await authService.LoadFromFileAsync();
if (!loadedFromFile)
if (loadedFromFile)
{
IsAuthenticated = false;
AppendLog("No valid auth.json found.");
return false;
return await ValidateCurrentAuthAsync();
}
return await ValidateCurrentAuthAsync(logAuthenticationMessage);
IsAuthenticated = false;
return false;
}
private bool ValidateConfiguredToolPathsOnStartup()
@ -1479,11 +1525,10 @@ public partial class MainWindowViewModel(
ConfigScreenMessage = "Configuration has invalid FFmpeg/FFprobe path values. Fix and save to continue.";
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return false;
}
private async Task<bool> ValidateCurrentAuthAsync(bool logAuthenticationMessage)
private async Task<bool> ValidateCurrentAuthAsync()
{
authService.ValidateCookieString();
UserEntities.User? user = await authService.ValidateAuthAsync();
@ -1491,35 +1536,13 @@ public partial class MainWindowViewModel(
{
authService.CurrentAuth = null;
IsAuthenticated = false;
if (File.Exists("auth.json") && !configService.CurrentConfig.DisableBrowserAuth)
{
File.Delete("auth.json");
}
AppendLog("Auth validation failed.");
return false;
}
string displayName = !string.IsNullOrWhiteSpace(user.Name) ? user.Name : "Unknown Name";
string displayUsername = !string.IsNullOrWhiteSpace(user.Username) ? user.Username : "Unknown Username";
if (HidePrivateInfo)
{
AuthenticatedUserDisplay = "[Hidden for Privacy]";
if (logAuthenticationMessage)
{
AppendLog("Authenticated as [Hidden for Privacy].");
}
}
else
{
AuthenticatedUserDisplay = $"{displayName} ({displayUsername})";
if (logAuthenticationMessage)
{
AppendLog($"Authenticated as {AuthenticatedUserDisplay}.");
}
}
AuthenticatedUserDisplay = HidePrivateInfo ? "[Hidden for Privacy]" : $"{displayName} ({displayUsername})";
IsAuthenticated = true;
return true;
}
@ -1564,8 +1587,7 @@ public partial class MainWindowViewModel(
}
CurrentScreen = AppScreen.UserSelection;
StatusMessage = $"Loaded {_allUsers.Count} users and {_allLists.Count} lists.";
AppendLog(StatusMessage);
AppendLog($"Loaded {_allUsers.Count} users and {_allLists.Count} lists.");
DownloadSelectedCommand.NotifyCanExecuteChanged();
DownloadPurchasedTabCommand.NotifyCanExecuteChanged();
SelectUsersFromListCommand.NotifyCanExecuteChanged();
@ -2032,14 +2054,12 @@ public partial class MainWindowViewModel(
private void SetLoading(string message)
{
LoadingMessage = message;
StatusMessage = message;
CurrentScreen = AppScreen.Loading;
}
private void ShowError(string message)
{
ErrorMessage = message;
StatusMessage = message;
CurrentScreen = AppScreen.Error;
AppendLog(message);
}

View File

@ -71,6 +71,20 @@
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="Button.link">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Foreground" Value="{DynamicResource PrimaryButtonBackgroundBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="Button.link:pointerover">
<Setter Property="Foreground" Value="{DynamicResource PrimaryButtonBackgroundHoverBrush}" />
</Style>
<Style Selector="TextBox.fileNameOverlayInput:not(:empty)">
<Setter Property="Foreground" Value="Transparent" />
</Style>
@ -1011,6 +1025,48 @@
Classes="primary"
IsEnabled="{Binding !IsBrowserLoginInProgress}"
Command="{Binding StartBrowserLoginCommand}" />
<Button Content="Manual Authentication"
Classes="secondary"
IsEnabled="{Binding !IsBrowserLoginInProgress}"
Command="{Binding OpenManualAuthScreenCommand}" />
</StackPanel>
</StackPanel>
<StackPanel IsVisible="{Binding IsManualAuthScreen}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="760"
Spacing="14">
<TextBlock HorizontalAlignment="Center"
FontSize="24"
FontWeight="SemiBold"
Text="Manual Authentication" />
<StackPanel HorizontalAlignment="Center" Spacing="8">
<TextBlock TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
Foreground="{DynamicResource TextSecondaryBrush}"
Text="{Binding ManualAuthInstructionsText}" />
<Button Classes="link"
HorizontalAlignment="Center"
Content="https://docs.ofdl.tools/config/auth/#legacy-methods"
Click="OnAuthLegacyMethodsClick" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
Text="{Binding ManualAuthScreenMessage}" />
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
<Button Content="Go Back"
Classes="secondary"
Command="{Binding GoBackToAuthScreenCommand}" />
<Button Content="Continue"
Classes="primary"
Command="{Binding ContinueWithManualAuthCommand}" />
</StackPanel>
</StackPanel>

View File

@ -196,6 +196,19 @@ public partial class MainWindow : Window
}
}
private async void OnAuthLegacyMethodsClick(object? sender, RoutedEventArgs e)
{
try
{
await WebLinkHelper.OpenOrCopyAsync(this, Constants.LegacyAuthDocumentationUrl,
dockerFeedbackAsync: ShowCopyToastAsync);
}
catch (Exception exception)
{
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
}
}
private void OnFaqClick(object? sender, RoutedEventArgs e)
{
FaqWindow faqWindow = new() { WindowStartupLocation = WindowStartupLocation.CenterOwner };