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

View File

@ -12,6 +12,8 @@ internal sealed class AvaloniaDownloadEventHandler(
Func<bool> isCancellationRequested, Func<bool> isCancellationRequested,
CancellationToken cancellationToken) : IDownloadEventHandler CancellationToken cancellationToken) : IDownloadEventHandler
{ {
private string _lastProgressDescription = string.Empty;
public CancellationToken CancellationToken { get; } = cancellationToken; public CancellationToken CancellationToken { get; } = cancellationToken;
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work) 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) Func<IProgressReporter, Task<T>> work)
{ {
ThrowIfCancellationRequested(); ThrowIfCancellationRequested();
_lastProgressDescription = description;
progressStart(description, maxValue, showSize); progressStart(description, maxValue, showSize);
try try
{ {
@ -87,7 +90,8 @@ internal sealed class AvaloniaDownloadEventHandler(
public void OnScrapeComplete(TimeSpan elapsed) public void OnScrapeComplete(TimeSpan elapsed)
{ {
ThrowIfCancellationRequested(); ThrowIfCancellationRequested();
activitySink($"Scrape completed in {elapsed.TotalMinutes:0.00} minutes."); string summary = BuildCompletionSummary(elapsed);
activitySink(summary);
} }
public void OnMessage(string message) public void OnMessage(string message)
@ -103,4 +107,21 @@ internal sealed class AvaloniaDownloadEventHandler(
throw new OperationCanceledException("Operation canceled by user."); 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; [ObservableProperty] private string _textValue = string.Empty;
private bool _isNormalizingFileNameFormatInput;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))] [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(InsertSelectedFileNameVariableCommand))]
private string? _selectedFileNameVariable; private string? _selectedFileNameVariable;
@ -370,18 +368,6 @@ public partial class ConfigFieldViewModel : ViewModelBase
partial void OnTextValueChanged(string value) 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 // Store actual value if not the privacy placeholder
if (value != "[Hidden for Privacy]") if (value != "[Hidden for Privacy]")
{ {

View File

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

View File

@ -428,6 +428,11 @@ public partial class MainWindowViewModel(
_isUpdatingAllUsersSelected = true; _isUpdatingAllUsersSelected = true;
try try
{ {
if (!string.IsNullOrWhiteSpace(SelectedListName))
{
SelectedListName = null;
}
foreach (SelectableUserViewModel user in AvailableUsers) foreach (SelectableUserViewModel user in AvailableUsers)
{ {
user.IsSelected = shouldSelectAll; user.IsSelected = shouldSelectAll;
@ -614,7 +619,8 @@ public partial class MainWindowViewModel(
{ {
if (!TryBuildConfig(out Config newConfig)) if (!TryBuildConfig(out Config newConfig))
{ {
StatusMessage = "Fix configuration validation errors and save again."; ConfigScreenMessage = "Fix configuration validation errors and save again.";
StatusMessage = ConfigScreenMessage;
return; return;
} }
@ -637,12 +643,12 @@ public partial class MainWindowViewModel(
ConfigScreenMessage = "Configuration saved."; ConfigScreenMessage = "Configuration saved.";
StatusMessage = "Configuration saved."; StatusMessage = "Configuration saved.";
if (!await ValidateEnvironmentAsync()) if (!await ValidateEnvironmentAsync(false))
{ {
return; return;
} }
await EnsureAuthenticationAndLoadUsersAsync(); await EnsureAuthenticationAndLoadUsersAsync(false);
} }
[RelayCommand] [RelayCommand]
@ -677,7 +683,7 @@ public partial class MainWindowViewModel(
await authService.SaveToFileAsync(); await authService.SaveToFileAsync();
bool isAuthValid = await ValidateCurrentAuthAsync(); bool isAuthValid = await ValidateCurrentAuthAsync(true);
if (!isAuthValid) if (!isAuthValid)
{ {
AuthScreenMessage = "Authentication is still invalid after login. Please retry."; AuthScreenMessage = "Authentication is still invalid after login. Please retry.";
@ -1041,12 +1047,17 @@ public partial class MainWindowViewModel(
return; return;
} }
if (!await ValidateEnvironmentAsync()) if (!ValidateConfiguredToolPathsOnStartup())
{ {
return; return;
} }
await EnsureAuthenticationAndLoadUsersAsync(); if (!await ValidateEnvironmentAsync(true))
{
return;
}
await EnsureAuthenticationAndLoadUsersAsync(true);
} }
private async Task RunSinglePostOrMessageDownloadAsync(SingleDownloadRequest request) 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 (!hasValidAuth)
{ {
if (configService.CurrentConfig.DisableBrowserAuth) if (configService.CurrentConfig.DisableBrowserAuth)
@ -1310,12 +1321,14 @@ public partial class MainWindowViewModel(
await LoadUsersAndListsAsync(); await LoadUsersAndListsAsync();
} }
private async Task<bool> ValidateEnvironmentAsync() private async Task<bool> ValidateEnvironmentAsync(bool logMissingCdmKeysWarning)
{ {
SetLoading("Validating environment..."); SetLoading("Validating environment...");
_startupResult = await startupService.ValidateEnvironmentAsync(); _startupResult = await startupService.ValidateEnvironmentAsync();
OnPropertyChanged(nameof(FfmpegVersion)); OnPropertyChanged(nameof(FfmpegVersion));
OnPropertyChanged(nameof(FfprobeVersion)); OnPropertyChanged(nameof(FfprobeVersion));
FfmpegPathError = string.Empty;
FfprobePathError = string.Empty;
if (!_startupResult.IsWindowsVersionValid) if (!_startupResult.IsWindowsVersionValid)
{ {
@ -1325,7 +1338,33 @@ public partial class MainWindowViewModel(
if (!_startupResult.FfmpegFound) 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; CurrentScreen = AppScreen.Config;
StatusMessage = ConfigScreenMessage; StatusMessage = ConfigScreenMessage;
return false; return false;
@ -1338,7 +1377,8 @@ public partial class MainWindowViewModel(
return false; return false;
} }
if (_startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing) if (logMissingCdmKeysWarning &&
(_startupResult.ClientIdBlobMissing || _startupResult.DevicePrivateKeyMissing))
{ {
AppendLog( AppendLog(
"CDM key files are missing. Fallback decrypt services will be used for DRM protected videos."); "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; return true;
} }
private async Task<bool> TryLoadAndValidateExistingAuthAsync() private async Task<bool> TryLoadAndValidateExistingAuthAsync(bool logAuthenticationMessage)
{ {
bool loadedFromFile = await authService.LoadFromFileAsync(); bool loadedFromFile = await authService.LoadFromFileAsync();
if (!loadedFromFile) if (!loadedFromFile)
@ -1357,10 +1397,40 @@ public partial class MainWindowViewModel(
return false; 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(); authService.ValidateCookieString();
UserEntities.User? user = await authService.ValidateAuthAsync(); UserEntities.User? user = await authService.ValidateAuthAsync();
@ -1383,13 +1453,19 @@ public partial class MainWindowViewModel(
if (HidePrivateInfo) if (HidePrivateInfo)
{ {
AuthenticatedUserDisplay = "[Hidden for Privacy]"; AuthenticatedUserDisplay = "[Hidden for Privacy]";
if (logAuthenticationMessage)
{
AppendLog("Authenticated as [Hidden for Privacy]."); AppendLog("Authenticated as [Hidden for Privacy].");
} }
}
else else
{ {
AuthenticatedUserDisplay = $"{displayName} ({displayUsername})"; AuthenticatedUserDisplay = $"{displayName} ({displayUsername})";
if (logAuthenticationMessage)
{
AppendLog($"Authenticated as {AuthenticatedUserDisplay}."); AppendLog($"Authenticated as {AuthenticatedUserDisplay}.");
} }
}
IsAuthenticated = true; IsAuthenticated = true;
return true; return true;
@ -1802,6 +1878,18 @@ public partial class MainWindowViewModel(
private static string EscapePathForConfig(string path) => private static string EscapePathForConfig(string path) =>
path.Replace(@"\", @"\\"); 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() private static string ResolveProgramVersion()
{ {
Version? version = Assembly.GetEntryAssembly()?.GetName().Version Version? version = Assembly.GetEntryAssembly()?.GetName().Version
@ -1916,6 +2004,11 @@ public partial class MainWindowViewModel(
{ {
if (e.PropertyName == nameof(SelectableUserViewModel.IsSelected)) if (e.PropertyName == nameof(SelectableUserViewModel.IsSelected))
{ {
if (!_isApplyingListSelection && !string.IsNullOrWhiteSpace(SelectedListName))
{
SelectedListName = null;
}
OnPropertyChanged(nameof(SelectedUsersSummary)); OnPropertyChanged(nameof(SelectedUsersSummary));
if (!_isUpdatingAllUsersSelected) if (!_isUpdatingAllUsersSelected)
{ {

View File

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