359 lines
11 KiB
C#
359 lines
11 KiB
C#
using System.Collections.Specialized;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.VisualTree;
|
|
using Avalonia.Threading;
|
|
using OF_DL.Helpers;
|
|
using OF_DL.Gui.Helpers;
|
|
using OF_DL.Gui.ViewModels;
|
|
using Serilog;
|
|
|
|
namespace OF_DL.Gui.Views;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
private bool _hasInitialized;
|
|
private bool _activityLogAutoScroll = true;
|
|
private bool _activityLogUserInteracted;
|
|
private bool _isActivityLogProgrammaticScroll;
|
|
private ScrollViewer? _activityLogScrollViewer;
|
|
private CancellationTokenSource? _copyToastCancellationTokenSource;
|
|
public MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico")));
|
|
|
|
// Start maximized if running in Docker
|
|
if (EnvironmentHelper.IsRunningInDocker())
|
|
{
|
|
WindowState = WindowState.Maximized;
|
|
}
|
|
|
|
Opened += OnOpened;
|
|
Closed += OnClosed;
|
|
}
|
|
|
|
private async void OnOpened(object? sender, EventArgs e)
|
|
{
|
|
if (_hasInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_hasInitialized = true;
|
|
InitializeActivityLogAutoScroll();
|
|
|
|
if (DataContext is MainWindowViewModel vm)
|
|
{
|
|
await vm.InitializeAsync();
|
|
}
|
|
}
|
|
|
|
private void OnClosed(object? sender, EventArgs e)
|
|
{
|
|
if (ViewModel != null)
|
|
{
|
|
ViewModel.ActivityLog.CollectionChanged -= OnActivityLogCollectionChanged;
|
|
}
|
|
|
|
if (_activityLogScrollViewer != null)
|
|
{
|
|
_activityLogScrollViewer.ScrollChanged -= OnActivityLogScrollChanged;
|
|
}
|
|
|
|
ActivityLogListBox.PointerWheelChanged -= OnActivityLogPointerInteracted;
|
|
ActivityLogListBox.PointerPressed -= OnActivityLogPointerInteracted;
|
|
|
|
_copyToastCancellationTokenSource?.Cancel();
|
|
_copyToastCancellationTokenSource?.Dispose();
|
|
_copyToastCancellationTokenSource = null;
|
|
}
|
|
|
|
private async void OnBrowseFfmpegPathClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TopLevel? topLevel = GetTopLevel(this);
|
|
if (topLevel?.StorageProvider == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IReadOnlyList<IStorageFile> selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
|
|
new FilePickerOpenOptions { Title = "Select FFmpeg executable", AllowMultiple = false });
|
|
|
|
IStorageFile? selectedFile = selectedFiles.FirstOrDefault();
|
|
if (selectedFile == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? localPath = selectedFile.TryGetLocalPath();
|
|
if (!string.IsNullOrWhiteSpace(localPath))
|
|
{
|
|
vm.SetFfmpegPath(localPath);
|
|
return;
|
|
}
|
|
|
|
vm.SetFfmpegPath(selectedFile.Name);
|
|
}
|
|
|
|
private async void OnBrowseFfprobePathClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TopLevel? topLevel = GetTopLevel(this);
|
|
if (topLevel?.StorageProvider == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IReadOnlyList<IStorageFile> selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
|
|
new FilePickerOpenOptions { Title = "Select FFprobe executable", AllowMultiple = false });
|
|
|
|
IStorageFile? selectedFile = selectedFiles.FirstOrDefault();
|
|
if (selectedFile == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? localPath = selectedFile.TryGetLocalPath();
|
|
if (!string.IsNullOrWhiteSpace(localPath))
|
|
{
|
|
vm.SetFfprobePath(localPath);
|
|
return;
|
|
}
|
|
|
|
vm.SetFfprobePath(selectedFile.Name);
|
|
}
|
|
|
|
private async void OnBrowseDownloadPathClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TopLevel? topLevel = GetTopLevel(this);
|
|
if (topLevel?.StorageProvider == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IReadOnlyList<IStorageFolder> selectedFolders = await topLevel.StorageProvider.OpenFolderPickerAsync(
|
|
new FolderPickerOpenOptions { Title = "Select download folder", AllowMultiple = false });
|
|
|
|
IStorageFolder? selectedFolder = selectedFolders.FirstOrDefault();
|
|
if (selectedFolder == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? localPath = selectedFolder.TryGetLocalPath();
|
|
if (!string.IsNullOrWhiteSpace(localPath))
|
|
{
|
|
vm.SetDownloadPath(localPath);
|
|
return;
|
|
}
|
|
|
|
vm.SetDownloadPath(selectedFolder.Name);
|
|
}
|
|
|
|
private async void OnJoinDiscordClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
await WebLinkHelper.OpenOrCopyAsync(this, Constants.DiscordInviteUrl,
|
|
dockerFeedbackAsync: ShowCopyToastAsync);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
|
}
|
|
}
|
|
|
|
private async void OnDocumentationClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
await WebLinkHelper.OpenOrCopyAsync(this, Constants.DocumentationUrl,
|
|
dockerFeedbackAsync: ShowCopyToastAsync);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
|
}
|
|
}
|
|
|
|
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 };
|
|
faqWindow.Show(this);
|
|
}
|
|
|
|
private void OnAboutClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not MainWindowViewModel vm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AboutWindow aboutWindow = new(vm.ProgramVersion, vm.FfmpegVersion, vm.FfprobeVersion)
|
|
{
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
|
};
|
|
aboutWindow.Show(this);
|
|
}
|
|
|
|
private async Task ShowCopyToastAsync(string message)
|
|
{
|
|
if (_copyToastCancellationTokenSource != null)
|
|
{
|
|
await _copyToastCancellationTokenSource.CancelAsync();
|
|
_copyToastCancellationTokenSource.Dispose();
|
|
}
|
|
|
|
CancellationTokenSource cancellationTokenSource = new();
|
|
_copyToastCancellationTokenSource = cancellationTokenSource;
|
|
|
|
CopyToastTextBlock.Text = message;
|
|
CopyToastBorder.Opacity = 0;
|
|
CopyToastBorder.IsVisible = true;
|
|
CopyToastBorder.Opacity = 1;
|
|
|
|
try
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(2), cancellationTokenSource.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CopyToastBorder.Opacity = 0;
|
|
|
|
try
|
|
{
|
|
await Task.Delay(TimeSpan.FromMilliseconds(180), cancellationTokenSource.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!cancellationTokenSource.IsCancellationRequested)
|
|
{
|
|
CopyToastBorder.IsVisible = false;
|
|
}
|
|
}
|
|
|
|
private void OnModalOverlayClicked(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
// Only handle clicks on the overlay itself (the Grid background)
|
|
if (DataContext is not MainWindowViewModel vm)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Execute cancel command on any open modal
|
|
vm.CreatorConfigEditor.ModalViewModel.CancelCommand.Execute(null);
|
|
vm.CancelSinglePostOrMessageCommand.Execute(null);
|
|
vm.CancelDownloadSelectionWarningCommand.Execute(null);
|
|
vm.CancelMissingCdmWarningCommand.Execute(null);
|
|
}
|
|
|
|
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e) =>
|
|
// Stop the event from bubbling up to the overlay
|
|
e.Handled = true;
|
|
|
|
private void InitializeActivityLogAutoScroll()
|
|
{
|
|
if (ViewModel != null)
|
|
{
|
|
ViewModel.ActivityLog.CollectionChanged -= OnActivityLogCollectionChanged;
|
|
ViewModel.ActivityLog.CollectionChanged += OnActivityLogCollectionChanged;
|
|
}
|
|
|
|
_activityLogScrollViewer = ActivityLogListBox.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
|
|
if (_activityLogScrollViewer != null)
|
|
{
|
|
_activityLogScrollViewer.ScrollChanged -= OnActivityLogScrollChanged;
|
|
_activityLogScrollViewer.ScrollChanged += OnActivityLogScrollChanged;
|
|
}
|
|
|
|
ActivityLogListBox.PointerWheelChanged -= OnActivityLogPointerInteracted;
|
|
ActivityLogListBox.PointerWheelChanged += OnActivityLogPointerInteracted;
|
|
ActivityLogListBox.PointerPressed -= OnActivityLogPointerInteracted;
|
|
ActivityLogListBox.PointerPressed += OnActivityLogPointerInteracted;
|
|
}
|
|
|
|
private void OnActivityLogCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (!_activityLogAutoScroll)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Dispatcher.UIThread.Post(ScrollActivityLogToBottom);
|
|
}
|
|
|
|
private void OnActivityLogPointerInteracted(object? sender, PointerEventArgs e) =>
|
|
_activityLogUserInteracted = true;
|
|
|
|
private void OnActivityLogScrollChanged(object? sender, ScrollChangedEventArgs e)
|
|
{
|
|
if (_isActivityLogProgrammaticScroll || _activityLogScrollViewer == null || !_activityLogUserInteracted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_activityLogAutoScroll = IsAtBottom(_activityLogScrollViewer);
|
|
_activityLogUserInteracted = false;
|
|
}
|
|
|
|
private void ScrollActivityLogToBottom()
|
|
{
|
|
if (ViewModel == null || ViewModel.ActivityLog.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isActivityLogProgrammaticScroll = true;
|
|
try
|
|
{
|
|
ActivityLogListBox.ScrollIntoView(ViewModel.ActivityLog[^1]);
|
|
}
|
|
finally
|
|
{
|
|
_isActivityLogProgrammaticScroll = false;
|
|
}
|
|
}
|
|
|
|
private static bool IsAtBottom(ScrollViewer scrollViewer, double tolerance = 2) =>
|
|
scrollViewer.Offset.Y + scrollViewer.Viewport.Height >= scrollViewer.Extent.Height - tolerance;
|
|
}
|