From 36dbb3de5d4b6b04622f51ea01dee721777945dc Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Wed, 18 Feb 2026 04:29:51 -0600 Subject: [PATCH] GUI improvements --- .../FileNameFormatOverlayTextBlock.cs | 25 ++--- .../Services/AvaloniaDownloadEventHandler.cs | 39 +++++++- OF DL.Gui/ViewModels/ConfigFieldViewModel.cs | 3 + OF DL.Gui/ViewModels/MainWindowViewModel.cs | 36 ++++++- OF DL.Gui/Views/MainWindow.axaml | 13 ++- OF DL.Gui/Views/MainWindow.axaml.cs | 98 ++++++++++++++++++- 6 files changed, 184 insertions(+), 30 deletions(-) diff --git a/OF DL.Gui/Controls/FileNameFormatOverlayTextBlock.cs b/OF DL.Gui/Controls/FileNameFormatOverlayTextBlock.cs index 56a75a0..6053fd1 100644 --- a/OF DL.Gui/Controls/FileNameFormatOverlayTextBlock.cs +++ b/OF DL.Gui/Controls/FileNameFormatOverlayTextBlock.cs @@ -1,6 +1,4 @@ -using System.Collections; using System.Collections.Specialized; -using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; @@ -14,7 +12,8 @@ namespace OF_DL.Gui.Controls; public class FileNameFormatOverlayTextBlock : TextBlock { public static readonly StyledProperty?> SegmentsProperty = - AvaloniaProperty.Register?>(nameof(Segments)); + AvaloniaProperty.Register?>( + nameof(Segments)); public static readonly StyledProperty SourceTextBoxProperty = AvaloniaProperty.Register(nameof(SourceTextBox)); @@ -68,10 +67,8 @@ public class FileNameFormatOverlayTextBlock : TextBlock private static void OnSourceTextBoxChanged( FileNameFormatOverlayTextBlock sender, - AvaloniaPropertyChangedEventArgs e) - { + AvaloniaPropertyChangedEventArgs e) => sender.AttachSourceTextBox(e.NewValue as TextBox); - } private void AttachSegmentsCollection(IEnumerable? segments) { @@ -93,10 +90,7 @@ public class FileNameFormatOverlayTextBlock : TextBlock _segmentsCollection = null; } - private void OnSegmentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - RebuildInlines(); - } + private void OnSegmentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildInlines(); private void AttachSourceTextBox(TextBox? textBox) { @@ -143,10 +137,7 @@ public class FileNameFormatOverlayTextBlock : TextBlock UpdateOverlayOffset(); } - private void OnSourceScrollChanged(object? sender, ScrollChangedEventArgs e) - { - UpdateOverlayOffset(); - } + private void OnSourceScrollChanged(object? sender, ScrollChangedEventArgs e) => UpdateOverlayOffset(); private void AttachScrollViewer(ScrollViewer? scrollViewer) { @@ -203,11 +194,7 @@ public class FileNameFormatOverlayTextBlock : TextBlock foreach (FileNameFormatSegmentViewModel segment in Segments) { - Run run = new() - { - Text = segment.Text ?? string.Empty, - Foreground = ParseForegroundBrush(segment.Foreground) - }; + Run run = new() { Text = segment.Text, Foreground = ParseForegroundBrush(segment.Foreground) }; Inlines.Add(run); } } diff --git a/OF DL.Gui/Services/AvaloniaDownloadEventHandler.cs b/OF DL.Gui/Services/AvaloniaDownloadEventHandler.cs index 432c96e..d0bbd5e 100644 --- a/OF DL.Gui/Services/AvaloniaDownloadEventHandler.cs +++ b/OF DL.Gui/Services/AvaloniaDownloadEventHandler.cs @@ -13,6 +13,10 @@ internal sealed class AvaloniaDownloadEventHandler( CancellationToken cancellationToken) : IDownloadEventHandler { private string _lastProgressDescription = string.Empty; + private string _activeUsername = string.Empty; + private DateTime _activeUserStartedAtUtc; + private readonly Dictionary _contentObjectCounts = new(StringComparer.Ordinal); + private bool _hasPerUserCompletionLogged; public CancellationToken CancellationToken { get; } = cancellationToken; @@ -51,6 +55,7 @@ internal sealed class AvaloniaDownloadEventHandler( public void OnContentFound(string contentType, int mediaCount, int objectCount) { ThrowIfCancellationRequested(); + _contentObjectCounts[contentType] = objectCount; progressStatusUpdate($"Found {mediaCount} media from {objectCount} {contentType}."); } @@ -63,6 +68,19 @@ internal sealed class AvaloniaDownloadEventHandler( public void OnDownloadComplete(string contentType, DownloadResult result) { ThrowIfCancellationRequested(); + if (!string.IsNullOrWhiteSpace(_activeUsername) && result.NewDownloads > 0) + { + if (_contentObjectCounts.TryGetValue(contentType, out int objectCount) && objectCount > 0) + { + activitySink( + $"Downloaded {result.NewDownloads} media from {objectCount} {contentType.ToLowerInvariant()}."); + } + else + { + activitySink($"Downloaded {result.NewDownloads} media from {contentType.ToLowerInvariant()}."); + } + } + progressStatusUpdate( $"{contentType} complete. Existing: {result.ExistingDownloads}, New: {result.NewDownloads}, Total: {result.TotalCount}."); } @@ -70,6 +88,9 @@ internal sealed class AvaloniaDownloadEventHandler( public void OnUserStarting(string username) { ThrowIfCancellationRequested(); + _activeUsername = username; + _activeUserStartedAtUtc = DateTime.UtcNow; + _hasPerUserCompletionLogged = false; activitySink($"Starting scrape for {username}."); progressStatusUpdate($"Scraping data for {username}..."); } @@ -77,19 +98,31 @@ internal sealed class AvaloniaDownloadEventHandler( public void OnUserComplete(string username, CreatorDownloadResult result) { ThrowIfCancellationRequested(); - activitySink( - $"Completed {username}. PaidPosts={result.PaidPostCount}, Posts={result.PostCount}, Archived={result.ArchivedCount}, Streams={result.StreamsCount}, Stories={result.StoriesCount}, Highlights={result.HighlightsCount}, Messages={result.MessagesCount}, PaidMessages={result.PaidMessagesCount}."); + TimeSpan elapsed = DateTime.UtcNow - _activeUserStartedAtUtc; + activitySink($"Completed {username} in {elapsed.TotalMinutes:0.0} minutes."); + _activeUsername = string.Empty; + _hasPerUserCompletionLogged = true; + _contentObjectCounts.Clear(); } public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount) { ThrowIfCancellationRequested(); - activitySink($"Purchased tab complete for {username}. PaidPosts={paidPostCount}, PaidMessages={paidMessagesCount}."); + TimeSpan elapsed = DateTime.UtcNow - _activeUserStartedAtUtc; + activitySink($"Completed {username} in {elapsed.TotalMinutes:0.0} minutes."); + _activeUsername = string.Empty; + _hasPerUserCompletionLogged = true; + _contentObjectCounts.Clear(); } public void OnScrapeComplete(TimeSpan elapsed) { ThrowIfCancellationRequested(); + if (_hasPerUserCompletionLogged) + { + return; + } + string summary = BuildCompletionSummary(elapsed); activitySink(summary); } diff --git a/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs b/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs index 61742ce..5da9742 100644 --- a/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs +++ b/OF DL.Gui/ViewModels/ConfigFieldViewModel.cs @@ -154,6 +154,9 @@ public partial class ConfigFieldViewModel : ViewModelBase public bool IsTimeoutField => string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal); + public bool IsHideMissingCdmKeysWarningField => + string.Equals(PropertyName, nameof(Config.HideMissingCdmKeysWarning), StringComparison.Ordinal); + public bool IsRegularTextInput => IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField && !IsFileNameFormatField; diff --git a/OF DL.Gui/ViewModels/MainWindowViewModel.cs b/OF DL.Gui/ViewModels/MainWindowViewModel.cs index d7695e4..f879f55 100644 --- a/OF DL.Gui/ViewModels/MainWindowViewModel.cs +++ b/OF DL.Gui/ViewModels/MainWindowViewModel.cs @@ -179,6 +179,8 @@ public partial class MainWindowViewModel( ["HelpBadgeBackgroundBrush"] = "#EAF0FB", ["HelpBadgeBorderBrush"] = "#C5D4EC", ["ErrorTextBrush"] = "#FF5A5A", + ["SuccessTextBrush"] = "#2C8A4B", + ["WarningTextBrush"] = "#B16A00", ["PreviewBackgroundBrush"] = "#F5F8FE", ["PreviewBorderBrush"] = "#D8E3F4", ["DangerSoftBackgroundBrush"] = "#FFE8E8", @@ -207,6 +209,8 @@ public partial class MainWindowViewModel( ["HelpBadgeBackgroundBrush"] = "#233145", ["HelpBadgeBorderBrush"] = "#3A4E6A", ["ErrorTextBrush"] = "#FF8C8C", + ["SuccessTextBrush"] = "#6BD98A", + ["WarningTextBrush"] = "#FFB357", ["PreviewBackgroundBrush"] = "#1B2636", ["PreviewBorderBrush"] = "#314359", ["DangerSoftBackgroundBrush"] = "#3A2024", @@ -382,6 +386,20 @@ public partial class MainWindowViewModel( public string DrmVideoDurationMatchThresholdPercentLabel => $"{Math.Round(DrmVideoDurationMatchThresholdPercent)}%"; + public string HideMissingCdmKeysWarningStatusText => + _startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing + ? "(CDM keys missing)" + : "(CDM keys detected)"; + + public IBrush HideMissingCdmKeysWarningStatusBrush => + ResolveThemeBrush( + _startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing + ? "WarningTextBrush" + : "SuccessTextBrush", + _startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing + ? "#FFB357" + : "#6BD98A"); + public string SelectedUsersSummary => $"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected"; @@ -940,7 +958,10 @@ public partial class MainWindowViewModel( private bool CanLogout() => IsAuthenticated && !IsDownloading; - private bool CanEditConfig() => CurrentScreen != AppScreen.Config; + private bool CanEditConfig() => + CurrentScreen != AppScreen.Config && + CurrentScreen != AppScreen.Loading && + !IsDownloading; partial void OnCurrentScreenChanged(AppScreen value) { @@ -1327,6 +1348,8 @@ public partial class MainWindowViewModel( _startupResult = await startupService.ValidateEnvironmentAsync(); OnPropertyChanged(nameof(FfmpegVersion)); OnPropertyChanged(nameof(FfprobeVersion)); + OnPropertyChanged(nameof(HideMissingCdmKeysWarningStatusText)); + OnPropertyChanged(nameof(HideMissingCdmKeysWarningStatusBrush)); FfmpegPathError = string.Empty; FfprobePathError = string.Empty; @@ -1878,6 +1901,17 @@ public partial class MainWindowViewModel( private static string EscapePathForConfig(string path) => path.Replace(@"\", @"\\"); + private static IBrush ResolveThemeBrush(string resourceKey, string fallbackColor) + { + if (Application.Current?.Resources.TryGetValue(resourceKey, out object? resource) == true && + resource is IBrush brush) + { + return brush; + } + + return new SolidColorBrush(Color.Parse(fallbackColor)); + } + private static string BuildToolPathError(string propertyName, string? configuredPath, string toolName) { string normalizedPath = NormalizePathForDisplay(configuredPath); diff --git a/OF DL.Gui/Views/MainWindow.axaml b/OF DL.Gui/Views/MainWindow.axaml index 001d3a2..748b009 100644 --- a/OF DL.Gui/Views/MainWindow.axaml +++ b/OF DL.Gui/Views/MainWindow.axaml @@ -731,8 +731,16 @@ ToolTip.Tip="{Binding HelpText}" /> - + + + + DataContext as MainWindowViewModel; public MainWindow() @@ -20,6 +27,7 @@ public partial class MainWindow : Window InitializeComponent(); Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico"))); Opened += OnOpened; + Closed += OnClosed; } private async void OnOpened(object? sender, EventArgs e) @@ -30,12 +38,30 @@ public partial class MainWindow : Window } _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; + } + private async void OnBrowseFfmpegPathClick(object? sender, RoutedEventArgs e) { if (DataContext is not MainWindowViewModel vm) @@ -140,10 +166,7 @@ public partial class MainWindow : Window private void OnFaqClick(object? sender, RoutedEventArgs e) { - FaqWindow faqWindow = new() - { - WindowStartupLocation = WindowStartupLocation.CenterOwner - }; + FaqWindow faqWindow = new() { WindowStartupLocation = WindowStartupLocation.CenterOwner }; faqWindow.Show(this); } @@ -184,7 +207,7 @@ public partial class MainWindow : Window } // Execute cancel command on any open modal - vm.CreatorConfigEditor.ModalViewModel?.CancelCommand?.Execute(null); + vm.CreatorConfigEditor.ModalViewModel.CancelCommand.Execute(null); vm.CancelSinglePostOrMessageCommand.Execute(null); vm.CancelMissingCdmWarningCommand.Execute(null); } @@ -192,4 +215,69 @@ public partial class MainWindow : Window 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; }