OF-DL/OF DL.Gui/Views/MainWindow.axaml.cs

345 lines
10 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 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.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;
}