Update progress messaging for consistency

This commit is contained in:
whimsical-c4lic0 2026-02-18 03:52:56 -06:00
parent 35bde51e7d
commit a74ebc810a
6 changed files with 155 additions and 72 deletions

View File

@ -177,7 +177,8 @@ public class DownloadOrchestrationService(
if (config.DownloadAvatarHeaderPhoto)
{
eventHandler.CancellationToken.ThrowIfCancellationRequested();
UserEntities.User? userInfo = await apiService.GetUserInfo($"/users/{username}", eventHandler.CancellationToken);
UserEntities.User? userInfo =
await apiService.GetUserInfo($"/users/{username}", eventHandler.CancellationToken);
if (userInfo != null)
{
await downloadService.DownloadAvatarHeader(userInfo.Avatar, userInfo.Header, path, username);
@ -256,7 +257,7 @@ public class DownloadOrchestrationService(
: tempStories.Count;
DownloadResult result = await eventHandler.WithProgressAsync(
$"Downloading {tempStories.Count} Stories", totalSize, config.ShowScrapeSize,
$"Downloading {tempStories.Count} stories", totalSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadStories(username, userId, path,
PaidPostIds.ToHashSet(), reporter));
@ -285,7 +286,7 @@ public class DownloadOrchestrationService(
: tempHighlights.Count;
DownloadResult result = await eventHandler.WithProgressAsync(
$"Downloading {tempHighlights.Count} Highlights", totalSize, config.ShowScrapeSize,
$"Downloading {tempHighlights.Count} highlights", totalSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadHighlights(username, userId, path,
PaidPostIds.ToHashSet(), reporter));
@ -359,9 +360,12 @@ public class DownloadOrchestrationService(
long totalSize = config.ShowScrapeSize
? await downloadService.CalculateTotalFileSize(post.SinglePosts.Values.ToList())
: post.SinglePosts.Count;
int postCount = post.SinglePostObjects.Count;
string postLabel = postCount == 1 ? "Post" : "Posts";
DownloadResult result = await eventHandler.WithProgressAsync(
"Downloading Post", totalSize, config.ShowScrapeSize,
$"Downloading {post.SinglePosts.Count} media from {postCount} {postLabel.ToLowerInvariant()}", totalSize,
config.ShowScrapeSize,
async reporter => await downloadService.DownloadSinglePost(username, path, users,
clientIdBlobMissing, devicePrivateKeyMissing, post, reporter));
@ -455,7 +459,7 @@ public class DownloadOrchestrationService(
: purchasedTabCollection.PaidPosts.PaidPosts.Count;
DownloadResult postResult = await eventHandler.WithProgressAsync(
$"Downloading {purchasedTabCollection.PaidPosts.PaidPosts.Count} Media from {purchasedTabCollection.PaidPosts.PaidPostObjects.Count} Paid Posts",
$"Downloading {purchasedTabCollection.PaidPosts.PaidPosts.Count} media from {purchasedTabCollection.PaidPosts.PaidPostObjects.Count} paid posts",
totalSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadPaidPostsPurchasedTab(
purchasedTabCollection.Username, path, users,
@ -483,7 +487,7 @@ public class DownloadOrchestrationService(
: purchasedTabCollection.PaidMessages.PaidMessages.Count;
DownloadResult msgResult = await eventHandler.WithProgressAsync(
$"Downloading {purchasedTabCollection.PaidMessages.PaidMessages.Count} Media from {purchasedTabCollection.PaidMessages.PaidMessageObjects.Count} Paid Messages",
$"Downloading {purchasedTabCollection.PaidMessages.PaidMessages.Count} media from {purchasedTabCollection.PaidMessages.PaidMessageObjects.Count} paid messages",
totalSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadPaidMessagesPurchasedTab(
purchasedTabCollection.Username, path, users,
@ -554,7 +558,7 @@ public class DownloadOrchestrationService(
: totalCount;
DownloadResult result = await eventHandler.WithProgressAsync(
$"Downloading {totalCount} Media from {messageCount} {messageLabel} ({paidCount} Paid + {previewCount} Preview)",
$"Downloading {totalCount} media from {messageCount} {messageLabel} ({paidCount} paid + {previewCount} preview)",
totalSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users,
clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter));
@ -574,7 +578,7 @@ public class DownloadOrchestrationService(
: previewCount;
DownloadResult previewResult = await eventHandler.WithProgressAsync(
$"Downloading {previewCount} Preview Media from {messageCount} {messageLabel}",
$"Downloading {previewCount} preview media from {messageCount} {messageLabel.ToLowerInvariant()}",
previewSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users,
clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter));
@ -594,7 +598,7 @@ public class DownloadOrchestrationService(
: paidCount;
DownloadResult result = await eventHandler.WithProgressAsync(
$"Downloading {paidCount} Paid Media from {messageCount} {messageLabel}",
$"Downloading {paidCount} paid media from {messageCount} {messageLabel.ToLowerInvariant()}",
totalSize, config.ShowScrapeSize,
async reporter => await downloadService.DownloadSinglePaidMessage(username, path, users,
clientIdBlobMissing, devicePrivateKeyMissing, singlePaidMessageCollection, reporter));
@ -658,7 +662,8 @@ public class DownloadOrchestrationService(
: mediaCount;
DownloadResult result = await eventHandler.WithProgressAsync(
$"Downloading {mediaCount} Media from {objectCount} {contentType}", totalSize, config.ShowScrapeSize,
$"Downloading {mediaCount} media from {objectCount} {contentType.ToLowerInvariant()}", totalSize,
config.ShowScrapeSize,
async reporter => await downloadData(data, reporter));
eventHandler.OnDownloadComplete(contentType, result);

View File

@ -12,6 +12,8 @@ internal sealed class AvaloniaDownloadEventHandler(
Func<bool> isCancellationRequested,
CancellationToken cancellationToken) : IDownloadEventHandler
{
private string _lastProgressDescription = string.Empty;
public CancellationToken CancellationToken { get; } = cancellationToken;
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
@ -33,6 +35,7 @@ internal sealed class AvaloniaDownloadEventHandler(
Func<IProgressReporter, Task<T>> work)
{
ThrowIfCancellationRequested();
_lastProgressDescription = description;
progressStart(description, maxValue, showSize);
try
{
@ -87,7 +90,8 @@ internal sealed class AvaloniaDownloadEventHandler(
public void OnScrapeComplete(TimeSpan elapsed)
{
ThrowIfCancellationRequested();
activitySink($"Scrape completed in {elapsed.TotalMinutes:0.00} minutes.");
string summary = BuildCompletionSummary(elapsed);
activitySink(summary);
}
public void OnMessage(string message)
@ -103,4 +107,21 @@ internal sealed class AvaloniaDownloadEventHandler(
throw new OperationCanceledException("Operation canceled by user.");
}
}
private string BuildCompletionSummary(TimeSpan elapsed)
{
if (string.IsNullOrWhiteSpace(_lastProgressDescription))
{
return $"Download completed in {elapsed.TotalMinutes:0.0} minutes.";
}
string normalized = _lastProgressDescription.Trim().TrimEnd('.');
if (normalized.StartsWith("Downloading ", StringComparison.OrdinalIgnoreCase))
{
string remainder = normalized["Downloading ".Length..].ToLowerInvariant();
return $"Downloaded {remainder} in {elapsed.TotalMinutes:0.0} minutes.";
}
return $"{normalized} in {elapsed.TotalMinutes:0.0} minutes.";
}
}

View File

@ -181,8 +181,6 @@ public partial class ConfigFieldViewModel : ViewModelBase
[ObservableProperty] private string _textValue = string.Empty;
private bool _isNormalizingFileNameFormatInput;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))]
private string? _selectedFileNameVariable;
@ -370,18 +368,6 @@ public partial class ConfigFieldViewModel : ViewModelBase
partial void OnTextValueChanged(string value)
{
if (IsFileNameFormatField && !_isNormalizingFileNameFormatInput)
{
string trimmedValue = value.Trim();
if (!string.Equals(value, trimmedValue, StringComparison.Ordinal))
{
_isNormalizingFileNameFormatInput = true;
TextValue = trimmedValue;
_isNormalizingFileNameFormatInput = false;
return;
}
}
// Store actual value if not the privacy placeholder
if (value != "[Hidden for Privacy]")
{

View File

@ -19,7 +19,6 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
private readonly Action<bool> _onClose;
private readonly Func<bool> _isUsernameDuplicate;
private bool _isNormalizingUsername;
private bool _isNormalizingFileNameFormat;
[ObservableProperty] private bool _isOpen;
[ObservableProperty] private bool _isEditMode;
@ -238,30 +237,22 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
}
partial void OnPaidPostFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => PaidPostFileNameFormat = trimmed,
HandleFileNameFormatChanged(
() => PaidPostFileNameFormatError = string.Empty,
UpdatePaidPostPreview);
partial void OnPostFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => PostFileNameFormat = trimmed,
HandleFileNameFormatChanged(
() => PostFileNameFormatError = string.Empty,
UpdatePostPreview);
partial void OnPaidMessageFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => PaidMessageFileNameFormat = trimmed,
HandleFileNameFormatChanged(
() => PaidMessageFileNameFormatError = string.Empty,
UpdatePaidMessagePreview);
partial void OnMessageFileNameFormatChanged(string value) =>
NormalizeFileNameFormat(
value,
trimmed => MessageFileNameFormat = trimmed,
HandleFileNameFormatChanged(
() => MessageFileNameFormatError = string.Empty,
UpdateMessagePreview);
@ -341,24 +332,10 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
isValid = false;
}
private void NormalizeFileNameFormat(
string value,
Action<string> setValue,
private static void HandleFileNameFormatChanged(
Action clearError,
Action updatePreview)
{
if (!_isNormalizingFileNameFormat)
{
string trimmedValue = value.Trim();
if (!string.Equals(value, trimmedValue, StringComparison.Ordinal))
{
_isNormalizingFileNameFormat = true;
setValue(trimmedValue);
_isNormalizingFileNameFormat = false;
return;
}
}
clearError();
updatePreview();
}

View File

@ -428,6 +428,11 @@ public partial class MainWindowViewModel(
_isUpdatingAllUsersSelected = true;
try
{
if (!string.IsNullOrWhiteSpace(SelectedListName))
{
SelectedListName = null;
}
foreach (SelectableUserViewModel user in AvailableUsers)
{
user.IsSelected = shouldSelectAll;
@ -614,7 +619,8 @@ public partial class MainWindowViewModel(
{
if (!TryBuildConfig(out Config newConfig))
{
StatusMessage = "Fix configuration validation errors and save again.";
ConfigScreenMessage = "Fix configuration validation errors and save again.";
StatusMessage = ConfigScreenMessage;
return;
}
@ -637,12 +643,12 @@ public partial class MainWindowViewModel(
ConfigScreenMessage = "Configuration saved.";
StatusMessage = "Configuration saved.";
if (!await ValidateEnvironmentAsync())
if (!await ValidateEnvironmentAsync(false))
{
return;
}
await EnsureAuthenticationAndLoadUsersAsync();
await EnsureAuthenticationAndLoadUsersAsync(false);
}
[RelayCommand]
@ -677,7 +683,7 @@ public partial class MainWindowViewModel(
await authService.SaveToFileAsync();
bool isAuthValid = await ValidateCurrentAuthAsync();
bool isAuthValid = await ValidateCurrentAuthAsync(true);
if (!isAuthValid)
{
AuthScreenMessage = "Authentication is still invalid after login. Please retry.";
@ -1041,12 +1047,17 @@ public partial class MainWindowViewModel(
return;
}
if (!await ValidateEnvironmentAsync())
if (!ValidateConfiguredToolPathsOnStartup())
{
return;
}
await EnsureAuthenticationAndLoadUsersAsync();
if (!await ValidateEnvironmentAsync(true))
{
return;
}
await EnsureAuthenticationAndLoadUsersAsync(true);
}
private async Task RunSinglePostOrMessageDownloadAsync(SingleDownloadRequest request)
@ -1288,9 +1299,9 @@ public partial class MainWindowViewModel(
}
}
private async Task EnsureAuthenticationAndLoadUsersAsync()
private async Task EnsureAuthenticationAndLoadUsersAsync(bool logAuthenticationMessage)
{
bool hasValidAuth = await TryLoadAndValidateExistingAuthAsync();
bool hasValidAuth = await TryLoadAndValidateExistingAuthAsync(logAuthenticationMessage);
if (!hasValidAuth)
{
if (configService.CurrentConfig.DisableBrowserAuth)
@ -1310,12 +1321,14 @@ public partial class MainWindowViewModel(
await LoadUsersAndListsAsync();
}
private async Task<bool> ValidateEnvironmentAsync()
private async Task<bool> ValidateEnvironmentAsync(bool logMissingCdmKeysWarning)
{
SetLoading("Validating environment...");
_startupResult = await startupService.ValidateEnvironmentAsync();
OnPropertyChanged(nameof(FfmpegVersion));
OnPropertyChanged(nameof(FfprobeVersion));
FfmpegPathError = string.Empty;
FfprobePathError = string.Empty;
if (!_startupResult.IsWindowsVersionValid)
{
@ -1325,7 +1338,33 @@ public partial class MainWindowViewModel(
if (!_startupResult.FfmpegFound)
{
ConfigScreenMessage = "FFmpeg was not found. Set a valid FFmpegPath before continuing.";
FfmpegPathError = BuildToolPathError(
nameof(Config.FFmpegPath),
configService.CurrentConfig.FFmpegPath,
"FFmpeg");
ConfigScreenMessage = "FFmpeg was not found. Fix FFmpeg Path and save to continue.";
BuildConfigFields(configService.CurrentConfig);
FfmpegPathError = BuildToolPathError(
nameof(Config.FFmpegPath),
configService.CurrentConfig.FFmpegPath,
"FFmpeg");
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return false;
}
if (!_startupResult.FfprobeFound)
{
FfprobePathError = BuildToolPathError(
nameof(Config.FFprobePath),
configService.CurrentConfig.FFprobePath,
"FFprobe");
ConfigScreenMessage = "FFprobe was not found. Fix FFprobe Path and save to continue.";
BuildConfigFields(configService.CurrentConfig);
FfprobePathError = BuildToolPathError(
nameof(Config.FFprobePath),
configService.CurrentConfig.FFprobePath,
"FFprobe");
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return false;
@ -1338,7 +1377,8 @@ public partial class MainWindowViewModel(
return false;
}
if (_startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing)
if (logMissingCdmKeysWarning &&
(_startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing))
{
AppendLog(
"CDM key files are missing. Fallback decrypt services will be used for DRM protected videos.");
@ -1347,7 +1387,7 @@ public partial class MainWindowViewModel(
return true;
}
private async Task<bool> TryLoadAndValidateExistingAuthAsync()
private async Task<bool> TryLoadAndValidateExistingAuthAsync(bool logAuthenticationMessage)
{
bool loadedFromFile = await authService.LoadFromFileAsync();
if (!loadedFromFile)
@ -1357,10 +1397,40 @@ public partial class MainWindowViewModel(
return false;
}
return await ValidateCurrentAuthAsync();
return await ValidateCurrentAuthAsync(logAuthenticationMessage);
}
private async Task<bool> ValidateCurrentAuthAsync()
private bool ValidateConfiguredToolPathsOnStartup()
{
IReadOnlyDictionary<string, string> validationErrors = ConfigValidationService.Validate(configService.CurrentConfig);
bool hasToolPathErrors = false;
FfmpegPathError = string.Empty;
FfprobePathError = string.Empty;
if (validationErrors.TryGetValue(nameof(Config.FFmpegPath), out string? ffmpegError))
{
FfmpegPathError = ffmpegError;
hasToolPathErrors = true;
}
if (validationErrors.TryGetValue(nameof(Config.FFprobePath), out string? ffprobeError))
{
FfprobePathError = ffprobeError;
hasToolPathErrors = true;
}
if (!hasToolPathErrors)
{
return true;
}
ConfigScreenMessage = "Configuration has invalid FFmpeg/FFprobe path values. Fix and save to continue.";
CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage;
return false;
}
private async Task<bool> ValidateCurrentAuthAsync(bool logAuthenticationMessage)
{
authService.ValidateCookieString();
UserEntities.User? user = await authService.ValidateAuthAsync();
@ -1383,13 +1453,19 @@ public partial class MainWindowViewModel(
if (HidePrivateInfo)
{
AuthenticatedUserDisplay = "[Hidden for Privacy]";
if (logAuthenticationMessage)
{
AppendLog("Authenticated as [Hidden for Privacy].");
}
}
else
{
AuthenticatedUserDisplay = $"{displayName} ({displayUsername})";
if (logAuthenticationMessage)
{
AppendLog($"Authenticated as {AuthenticatedUserDisplay}.");
}
}
IsAuthenticated = true;
return true;
@ -1802,6 +1878,18 @@ public partial class MainWindowViewModel(
private static string EscapePathForConfig(string path) =>
path.Replace(@"\", @"\\");
private static string BuildToolPathError(string propertyName, string? configuredPath, string toolName)
{
string normalizedPath = NormalizePathForDisplay(configuredPath);
if (string.IsNullOrWhiteSpace(normalizedPath))
{
return
$"{toolName} was not found automatically. Set {propertyName} to a valid executable path or add {toolName.ToLowerInvariant()} to PATH.";
}
return $"{propertyName} does not point to an existing file: {normalizedPath}";
}
private static string ResolveProgramVersion()
{
Version? version = Assembly.GetEntryAssembly()?.GetName().Version
@ -1916,6 +2004,11 @@ public partial class MainWindowViewModel(
{
if (e.PropertyName == nameof(SelectableUserViewModel.IsSelected))
{
if (!_isApplyingListSelection && !string.IsNullOrWhiteSpace(SelectedListName))
{
SelectedListName = null;
}
OnPropertyChanged(nameof(SelectedUsersSummary));
if (!_isUpdatingAllUsersSelected)
{

View File

@ -1065,9 +1065,10 @@
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectableUserViewModel">
<CheckBox Content="{Binding Username}"
IsChecked="{Binding IsSelected}"
HorizontalAlignment="Stretch" />
<CheckBox IsChecked="{Binding IsSelected}"
HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Username}" />
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>