forked from sim0n00ps/OF-DL
GUI improvements
This commit is contained in:
parent
a74ebc810a
commit
36dbb3de5d
@ -1,6 +1,4 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Documents;
|
using Avalonia.Controls.Documents;
|
||||||
@ -14,7 +12,8 @@ namespace OF_DL.Gui.Controls;
|
|||||||
public class FileNameFormatOverlayTextBlock : TextBlock
|
public class FileNameFormatOverlayTextBlock : TextBlock
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<IEnumerable<FileNameFormatSegmentViewModel>?> SegmentsProperty =
|
public static readonly StyledProperty<IEnumerable<FileNameFormatSegmentViewModel>?> SegmentsProperty =
|
||||||
AvaloniaProperty.Register<FileNameFormatOverlayTextBlock, IEnumerable<FileNameFormatSegmentViewModel>?>(nameof(Segments));
|
AvaloniaProperty.Register<FileNameFormatOverlayTextBlock, IEnumerable<FileNameFormatSegmentViewModel>?>(
|
||||||
|
nameof(Segments));
|
||||||
|
|
||||||
public static readonly StyledProperty<TextBox?> SourceTextBoxProperty =
|
public static readonly StyledProperty<TextBox?> SourceTextBoxProperty =
|
||||||
AvaloniaProperty.Register<FileNameFormatOverlayTextBlock, TextBox?>(nameof(SourceTextBox));
|
AvaloniaProperty.Register<FileNameFormatOverlayTextBlock, TextBox?>(nameof(SourceTextBox));
|
||||||
@ -68,10 +67,8 @@ public class FileNameFormatOverlayTextBlock : TextBlock
|
|||||||
|
|
||||||
private static void OnSourceTextBoxChanged(
|
private static void OnSourceTextBoxChanged(
|
||||||
FileNameFormatOverlayTextBlock sender,
|
FileNameFormatOverlayTextBlock sender,
|
||||||
AvaloniaPropertyChangedEventArgs e)
|
AvaloniaPropertyChangedEventArgs e) =>
|
||||||
{
|
|
||||||
sender.AttachSourceTextBox(e.NewValue as TextBox);
|
sender.AttachSourceTextBox(e.NewValue as TextBox);
|
||||||
}
|
|
||||||
|
|
||||||
private void AttachSegmentsCollection(IEnumerable<FileNameFormatSegmentViewModel>? segments)
|
private void AttachSegmentsCollection(IEnumerable<FileNameFormatSegmentViewModel>? segments)
|
||||||
{
|
{
|
||||||
@ -93,10 +90,7 @@ public class FileNameFormatOverlayTextBlock : TextBlock
|
|||||||
_segmentsCollection = null;
|
_segmentsCollection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSegmentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void OnSegmentsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildInlines();
|
||||||
{
|
|
||||||
RebuildInlines();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AttachSourceTextBox(TextBox? textBox)
|
private void AttachSourceTextBox(TextBox? textBox)
|
||||||
{
|
{
|
||||||
@ -143,10 +137,7 @@ public class FileNameFormatOverlayTextBlock : TextBlock
|
|||||||
UpdateOverlayOffset();
|
UpdateOverlayOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSourceScrollChanged(object? sender, ScrollChangedEventArgs e)
|
private void OnSourceScrollChanged(object? sender, ScrollChangedEventArgs e) => UpdateOverlayOffset();
|
||||||
{
|
|
||||||
UpdateOverlayOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AttachScrollViewer(ScrollViewer? scrollViewer)
|
private void AttachScrollViewer(ScrollViewer? scrollViewer)
|
||||||
{
|
{
|
||||||
@ -203,11 +194,7 @@ public class FileNameFormatOverlayTextBlock : TextBlock
|
|||||||
|
|
||||||
foreach (FileNameFormatSegmentViewModel segment in Segments)
|
foreach (FileNameFormatSegmentViewModel segment in Segments)
|
||||||
{
|
{
|
||||||
Run run = new()
|
Run run = new() { Text = segment.Text, Foreground = ParseForegroundBrush(segment.Foreground) };
|
||||||
{
|
|
||||||
Text = segment.Text ?? string.Empty,
|
|
||||||
Foreground = ParseForegroundBrush(segment.Foreground)
|
|
||||||
};
|
|
||||||
Inlines.Add(run);
|
Inlines.Add(run);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,10 @@ internal sealed class AvaloniaDownloadEventHandler(
|
|||||||
CancellationToken cancellationToken) : IDownloadEventHandler
|
CancellationToken cancellationToken) : IDownloadEventHandler
|
||||||
{
|
{
|
||||||
private string _lastProgressDescription = string.Empty;
|
private string _lastProgressDescription = string.Empty;
|
||||||
|
private string _activeUsername = string.Empty;
|
||||||
|
private DateTime _activeUserStartedAtUtc;
|
||||||
|
private readonly Dictionary<string, int> _contentObjectCounts = new(StringComparer.Ordinal);
|
||||||
|
private bool _hasPerUserCompletionLogged;
|
||||||
|
|
||||||
public CancellationToken CancellationToken { get; } = cancellationToken;
|
public CancellationToken CancellationToken { get; } = cancellationToken;
|
||||||
|
|
||||||
@ -51,6 +55,7 @@ internal sealed class AvaloniaDownloadEventHandler(
|
|||||||
public void OnContentFound(string contentType, int mediaCount, int objectCount)
|
public void OnContentFound(string contentType, int mediaCount, int objectCount)
|
||||||
{
|
{
|
||||||
ThrowIfCancellationRequested();
|
ThrowIfCancellationRequested();
|
||||||
|
_contentObjectCounts[contentType] = objectCount;
|
||||||
progressStatusUpdate($"Found {mediaCount} media from {objectCount} {contentType}.");
|
progressStatusUpdate($"Found {mediaCount} media from {objectCount} {contentType}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +68,19 @@ internal sealed class AvaloniaDownloadEventHandler(
|
|||||||
public void OnDownloadComplete(string contentType, DownloadResult result)
|
public void OnDownloadComplete(string contentType, DownloadResult result)
|
||||||
{
|
{
|
||||||
ThrowIfCancellationRequested();
|
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(
|
progressStatusUpdate(
|
||||||
$"{contentType} complete. Existing: {result.ExistingDownloads}, New: {result.NewDownloads}, Total: {result.TotalCount}.");
|
$"{contentType} complete. Existing: {result.ExistingDownloads}, New: {result.NewDownloads}, Total: {result.TotalCount}.");
|
||||||
}
|
}
|
||||||
@ -70,6 +88,9 @@ internal sealed class AvaloniaDownloadEventHandler(
|
|||||||
public void OnUserStarting(string username)
|
public void OnUserStarting(string username)
|
||||||
{
|
{
|
||||||
ThrowIfCancellationRequested();
|
ThrowIfCancellationRequested();
|
||||||
|
_activeUsername = username;
|
||||||
|
_activeUserStartedAtUtc = DateTime.UtcNow;
|
||||||
|
_hasPerUserCompletionLogged = false;
|
||||||
activitySink($"Starting scrape for {username}.");
|
activitySink($"Starting scrape for {username}.");
|
||||||
progressStatusUpdate($"Scraping data for {username}...");
|
progressStatusUpdate($"Scraping data for {username}...");
|
||||||
}
|
}
|
||||||
@ -77,19 +98,31 @@ internal sealed class AvaloniaDownloadEventHandler(
|
|||||||
public void OnUserComplete(string username, CreatorDownloadResult result)
|
public void OnUserComplete(string username, CreatorDownloadResult result)
|
||||||
{
|
{
|
||||||
ThrowIfCancellationRequested();
|
ThrowIfCancellationRequested();
|
||||||
activitySink(
|
TimeSpan elapsed = DateTime.UtcNow - _activeUserStartedAtUtc;
|
||||||
$"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}.");
|
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)
|
public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount)
|
||||||
{
|
{
|
||||||
ThrowIfCancellationRequested();
|
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)
|
public void OnScrapeComplete(TimeSpan elapsed)
|
||||||
{
|
{
|
||||||
ThrowIfCancellationRequested();
|
ThrowIfCancellationRequested();
|
||||||
|
if (_hasPerUserCompletionLogged)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string summary = BuildCompletionSummary(elapsed);
|
string summary = BuildCompletionSummary(elapsed);
|
||||||
activitySink(summary);
|
activitySink(summary);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -154,6 +154,9 @@ public partial class ConfigFieldViewModel : ViewModelBase
|
|||||||
public bool IsTimeoutField =>
|
public bool IsTimeoutField =>
|
||||||
string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal);
|
string.Equals(PropertyName, nameof(Config.Timeout), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public bool IsHideMissingCdmKeysWarningField =>
|
||||||
|
string.Equals(PropertyName, nameof(Config.HideMissingCdmKeysWarning), StringComparison.Ordinal);
|
||||||
|
|
||||||
public bool IsRegularTextInput =>
|
public bool IsRegularTextInput =>
|
||||||
IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField && !IsFileNameFormatField;
|
IsTextInput && !IsIgnoredUsersListField && !IsCreatorConfigsField && !IsFileNameFormatField;
|
||||||
|
|
||||||
|
|||||||
@ -179,6 +179,8 @@ public partial class MainWindowViewModel(
|
|||||||
["HelpBadgeBackgroundBrush"] = "#EAF0FB",
|
["HelpBadgeBackgroundBrush"] = "#EAF0FB",
|
||||||
["HelpBadgeBorderBrush"] = "#C5D4EC",
|
["HelpBadgeBorderBrush"] = "#C5D4EC",
|
||||||
["ErrorTextBrush"] = "#FF5A5A",
|
["ErrorTextBrush"] = "#FF5A5A",
|
||||||
|
["SuccessTextBrush"] = "#2C8A4B",
|
||||||
|
["WarningTextBrush"] = "#B16A00",
|
||||||
["PreviewBackgroundBrush"] = "#F5F8FE",
|
["PreviewBackgroundBrush"] = "#F5F8FE",
|
||||||
["PreviewBorderBrush"] = "#D8E3F4",
|
["PreviewBorderBrush"] = "#D8E3F4",
|
||||||
["DangerSoftBackgroundBrush"] = "#FFE8E8",
|
["DangerSoftBackgroundBrush"] = "#FFE8E8",
|
||||||
@ -207,6 +209,8 @@ public partial class MainWindowViewModel(
|
|||||||
["HelpBadgeBackgroundBrush"] = "#233145",
|
["HelpBadgeBackgroundBrush"] = "#233145",
|
||||||
["HelpBadgeBorderBrush"] = "#3A4E6A",
|
["HelpBadgeBorderBrush"] = "#3A4E6A",
|
||||||
["ErrorTextBrush"] = "#FF8C8C",
|
["ErrorTextBrush"] = "#FF8C8C",
|
||||||
|
["SuccessTextBrush"] = "#6BD98A",
|
||||||
|
["WarningTextBrush"] = "#FFB357",
|
||||||
["PreviewBackgroundBrush"] = "#1B2636",
|
["PreviewBackgroundBrush"] = "#1B2636",
|
||||||
["PreviewBorderBrush"] = "#314359",
|
["PreviewBorderBrush"] = "#314359",
|
||||||
["DangerSoftBackgroundBrush"] = "#3A2024",
|
["DangerSoftBackgroundBrush"] = "#3A2024",
|
||||||
@ -382,6 +386,20 @@ public partial class MainWindowViewModel(
|
|||||||
public string DrmVideoDurationMatchThresholdPercentLabel =>
|
public string DrmVideoDurationMatchThresholdPercentLabel =>
|
||||||
$"{Math.Round(DrmVideoDurationMatchThresholdPercent)}%";
|
$"{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 =>
|
public string SelectedUsersSummary =>
|
||||||
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
|
$"{AvailableUsers.Count(user => user.IsSelected)} / {AvailableUsers.Count} selected";
|
||||||
|
|
||||||
@ -940,7 +958,10 @@ public partial class MainWindowViewModel(
|
|||||||
|
|
||||||
private bool CanLogout() => IsAuthenticated && !IsDownloading;
|
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)
|
partial void OnCurrentScreenChanged(AppScreen value)
|
||||||
{
|
{
|
||||||
@ -1327,6 +1348,8 @@ public partial class MainWindowViewModel(
|
|||||||
_startupResult = await startupService.ValidateEnvironmentAsync();
|
_startupResult = await startupService.ValidateEnvironmentAsync();
|
||||||
OnPropertyChanged(nameof(FfmpegVersion));
|
OnPropertyChanged(nameof(FfmpegVersion));
|
||||||
OnPropertyChanged(nameof(FfprobeVersion));
|
OnPropertyChanged(nameof(FfprobeVersion));
|
||||||
|
OnPropertyChanged(nameof(HideMissingCdmKeysWarningStatusText));
|
||||||
|
OnPropertyChanged(nameof(HideMissingCdmKeysWarningStatusBrush));
|
||||||
FfmpegPathError = string.Empty;
|
FfmpegPathError = string.Empty;
|
||||||
FfprobePathError = string.Empty;
|
FfprobePathError = string.Empty;
|
||||||
|
|
||||||
@ -1878,6 +1901,17 @@ public partial class MainWindowViewModel(
|
|||||||
private static string EscapePathForConfig(string path) =>
|
private static string EscapePathForConfig(string path) =>
|
||||||
path.Replace(@"\", @"\\");
|
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)
|
private static string BuildToolPathError(string propertyName, string? configuredPath, string toolName)
|
||||||
{
|
{
|
||||||
string normalizedPath = NormalizePathForDisplay(configuredPath);
|
string normalizedPath = NormalizePathForDisplay(configuredPath);
|
||||||
|
|||||||
@ -731,8 +731,16 @@
|
|||||||
ToolTip.Tip="{Binding HelpText}" />
|
ToolTip.Tip="{Binding HelpText}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackPanel Grid.Column="1" Spacing="4">
|
<StackPanel Grid.Column="1" Spacing="4">
|
||||||
<CheckBox IsVisible="{Binding IsBoolean}"
|
<StackPanel IsVisible="{Binding IsBoolean}"
|
||||||
IsChecked="{Binding BoolValue}" />
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<CheckBox IsChecked="{Binding BoolValue}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock IsVisible="{Binding IsHideMissingCdmKeysWarningField}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding ViewModel.HideMissingCdmKeysWarningStatusText, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}"
|
||||||
|
Foreground="{Binding ViewModel.HideMissingCdmKeysWarningStatusBrush, RelativeSource={RelativeSource AncestorType=views:MainWindow}}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<ComboBox IsVisible="{Binding IsEnum}"
|
<ComboBox IsVisible="{Binding IsEnum}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
@ -1084,6 +1092,7 @@
|
|||||||
<TextBlock Grid.Row="0" FontWeight="SemiBold"
|
<TextBlock Grid.Row="0" FontWeight="SemiBold"
|
||||||
Foreground="{DynamicResource TextPrimaryBrush}" Text="Activity Log" />
|
Foreground="{DynamicResource TextPrimaryBrush}" Text="Activity Log" />
|
||||||
<ListBox Grid.Row="1"
|
<ListBox Grid.Row="1"
|
||||||
|
x:Name="ActivityLogListBox"
|
||||||
Margin="0,8,0,0"
|
Margin="0,8,0,0"
|
||||||
ItemsSource="{Binding ActivityLog}"
|
ItemsSource="{Binding ActivityLog}"
|
||||||
Background="{DynamicResource SurfaceBackgroundBrush}"
|
Background="{DynamicResource SurfaceBackgroundBrush}"
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using Avalonia.Threading;
|
||||||
using OF_DL.Gui.ViewModels;
|
using OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
namespace OF_DL.Gui.Views;
|
namespace OF_DL.Gui.Views;
|
||||||
@ -13,6 +16,10 @@ public partial class MainWindow : Window
|
|||||||
private const string DiscordInviteUrl = "https://discord.com/invite/6bUW8EJ53j";
|
private const string DiscordInviteUrl = "https://discord.com/invite/6bUW8EJ53j";
|
||||||
private const string DocumentationUrl = "https://docs.ofdl.tools/";
|
private const string DocumentationUrl = "https://docs.ofdl.tools/";
|
||||||
private bool _hasInitialized;
|
private bool _hasInitialized;
|
||||||
|
private bool _activityLogAutoScroll = true;
|
||||||
|
private bool _activityLogUserInteracted;
|
||||||
|
private bool _isActivityLogProgrammaticScroll;
|
||||||
|
private ScrollViewer? _activityLogScrollViewer;
|
||||||
public MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
|
public MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
@ -20,6 +27,7 @@ public partial class MainWindow : Window
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico")));
|
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico")));
|
||||||
Opened += OnOpened;
|
Opened += OnOpened;
|
||||||
|
Closed += OnClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnOpened(object? sender, EventArgs e)
|
private async void OnOpened(object? sender, EventArgs e)
|
||||||
@ -30,12 +38,30 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
_hasInitialized = true;
|
_hasInitialized = true;
|
||||||
|
InitializeActivityLogAutoScroll();
|
||||||
|
|
||||||
if (DataContext is MainWindowViewModel vm)
|
if (DataContext is MainWindowViewModel vm)
|
||||||
{
|
{
|
||||||
await vm.InitializeAsync();
|
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)
|
private async void OnBrowseFfmpegPathClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not MainWindowViewModel vm)
|
if (DataContext is not MainWindowViewModel vm)
|
||||||
@ -140,10 +166,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void OnFaqClick(object? sender, RoutedEventArgs e)
|
private void OnFaqClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
FaqWindow faqWindow = new()
|
FaqWindow faqWindow = new() { WindowStartupLocation = WindowStartupLocation.CenterOwner };
|
||||||
{
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
|
||||||
};
|
|
||||||
faqWindow.Show(this);
|
faqWindow.Show(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +207,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute cancel command on any open modal
|
// Execute cancel command on any open modal
|
||||||
vm.CreatorConfigEditor.ModalViewModel?.CancelCommand?.Execute(null);
|
vm.CreatorConfigEditor.ModalViewModel.CancelCommand.Execute(null);
|
||||||
vm.CancelSinglePostOrMessageCommand.Execute(null);
|
vm.CancelSinglePostOrMessageCommand.Execute(null);
|
||||||
vm.CancelMissingCdmWarningCommand.Execute(null);
|
vm.CancelMissingCdmWarningCommand.Execute(null);
|
||||||
}
|
}
|
||||||
@ -192,4 +215,69 @@ public partial class MainWindow : Window
|
|||||||
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e) =>
|
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e) =>
|
||||||
// Stop the event from bubbling up to the overlay
|
// Stop the event from bubbling up to the overlay
|
||||||
e.Handled = true;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user