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 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 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 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().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; }