feat: Implement AI CSV overwrite confirmation and update CSV output path based on destination
This commit is contained in:
parent
6e05869b04
commit
f3ac1ea920
3 changed files with 214 additions and 0 deletions
|
|
@ -96,6 +96,45 @@ public class DataModelCharacterizationTests
|
||||||
model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
|
model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DestinationPathChange_UpdatesAiCsvFileNameToMatchFinalFolderSegment()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
model.CsvOutputPath = @"K:\various\catalogtest\aioutput\test2.csv";
|
||||||
|
|
||||||
|
model.DestinationPath = @"K:\various\catalogtest\Dest\03.KM_8_A\";
|
||||||
|
|
||||||
|
model.CsvOutputPath.ShouldBe(@"K:\various\catalogtest\aioutput\03.KM_8_A.csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ConfirmAiCsvOverwriteIfNeededAsync_CanCancelWhenCsvAlreadyExists()
|
||||||
|
{
|
||||||
|
using var tempDirectory = new TemporaryDirectory();
|
||||||
|
var csvPath = Path.Combine(tempDirectory.Path, "existing.csv");
|
||||||
|
File.WriteAllText(csvPath, "existing");
|
||||||
|
|
||||||
|
var model = CreateModel();
|
||||||
|
model.CsvOutputPath = csvPath;
|
||||||
|
|
||||||
|
string? requestedTitle = null;
|
||||||
|
string? requestedMessage = null;
|
||||||
|
model.ConfirmAiCsvOverwriteAsync = (title, message) =>
|
||||||
|
{
|
||||||
|
requestedTitle = title;
|
||||||
|
requestedMessage = message;
|
||||||
|
return Task.FromResult(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
var shouldContinue = await model.ConfirmAiCsvOverwriteIfNeededAsync();
|
||||||
|
|
||||||
|
shouldContinue.ShouldBeFalse();
|
||||||
|
requestedTitle.ShouldBe("File CSV gia esistente");
|
||||||
|
requestedMessage.ShouldNotBeNull();
|
||||||
|
requestedMessage.ShouldContain("Vuoi sovrascriverlo?");
|
||||||
|
requestedMessage.ShouldContain(csvPath);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void AiChildChange_RaisesDataModelPropertyChanged()
|
public void AiChildChange_RaisesDataModelPropertyChanged()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ public partial class AvaloniaMainWindow : Window
|
||||||
|
|
||||||
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
||||||
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
||||||
|
_model.ConfirmAiCsvOverwriteAsync = ShowConfirmationDialogAsync;
|
||||||
|
|
||||||
_model.SelectSourceFolderRequested += async (_, _) =>
|
_model.SelectSourceFolderRequested += async (_, _) =>
|
||||||
{
|
{
|
||||||
|
|
@ -283,6 +284,25 @@ public partial class AvaloniaMainWindow : Window
|
||||||
await dialog.ShowDialog(this);
|
await dialog.ShowDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowConfirmationDialogAsync(string title, string message)
|
||||||
|
{
|
||||||
|
var dialog = new Window
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Width = 520,
|
||||||
|
CanResize = false,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||||
|
SizeToContent = SizeToContent.Height
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.Content = BuildConfirmationDialogContent(
|
||||||
|
message,
|
||||||
|
() => dialog.Close(true),
|
||||||
|
() => dialog.Close(false));
|
||||||
|
|
||||||
|
return await dialog.ShowDialog<bool>(this);
|
||||||
|
}
|
||||||
|
|
||||||
private static Control BuildMessageDialogContent(string message, Action closeDialog)
|
private static Control BuildMessageDialogContent(string message, Action closeDialog)
|
||||||
{
|
{
|
||||||
var layout = new StackPanel
|
var layout = new StackPanel
|
||||||
|
|
@ -310,4 +330,46 @@ public partial class AvaloniaMainWindow : Window
|
||||||
layout.Children.Add(closeButton);
|
layout.Children.Add(closeButton);
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Control BuildConfirmationDialogContent(string message, Action confirmDialog, Action cancelDialog)
|
||||||
|
{
|
||||||
|
var layout = new StackPanel
|
||||||
|
{
|
||||||
|
Margin = new Thickness(16),
|
||||||
|
Spacing = 12
|
||||||
|
};
|
||||||
|
|
||||||
|
layout.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = message,
|
||||||
|
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
|
||||||
|
MaxWidth = 460
|
||||||
|
});
|
||||||
|
|
||||||
|
var buttons = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
Spacing = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
var cancelButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Annulla",
|
||||||
|
MinWidth = 96
|
||||||
|
};
|
||||||
|
cancelButton.Click += (_, _) => cancelDialog();
|
||||||
|
|
||||||
|
var confirmButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Sovrascrivi",
|
||||||
|
MinWidth = 96
|
||||||
|
};
|
||||||
|
confirmButton.Click += (_, _) => confirmDialog();
|
||||||
|
|
||||||
|
buttons.Children.Add(cancelButton);
|
||||||
|
buttons.Children.Add(confirmButton);
|
||||||
|
layout.Children.Add(buttons);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ using System.Text.RegularExpressions;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace ImageCatalog_2
|
namespace ImageCatalog_2
|
||||||
{
|
{
|
||||||
|
|
@ -82,6 +83,8 @@ namespace ImageCatalog_2
|
||||||
|
|
||||||
private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary);
|
private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary);
|
||||||
|
|
||||||
|
private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente";
|
||||||
|
|
||||||
// ComboBox collections
|
// ComboBox collections
|
||||||
public List<string> AvailableFonts { get; }
|
public List<string> AvailableFonts { get; }
|
||||||
public List<string> VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" };
|
public List<string> VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" };
|
||||||
|
|
@ -145,6 +148,12 @@ namespace ImageCatalog_2
|
||||||
|
|
||||||
private async Task StartAiAsync()
|
private async Task StartAiAsync()
|
||||||
{
|
{
|
||||||
|
if (!await ConfirmAiCsvOverwriteIfNeededAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MainToken = new CancellationTokenSource();
|
MainToken = new CancellationTokenSource();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -267,6 +276,8 @@ namespace ImageCatalog_2
|
||||||
set => _ai.CsvOutputPath = value;
|
set => _ai.CsvOutputPath = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Func<string, string, Task<bool>>? ConfirmAiCsvOverwriteAsync { get; set; }
|
||||||
|
|
||||||
public bool UseNumberAiGpu
|
public bool UseNumberAiGpu
|
||||||
{
|
{
|
||||||
get => _ai.UseNumberAiGpu;
|
get => _ai.UseNumberAiGpu;
|
||||||
|
|
@ -641,6 +652,11 @@ namespace ImageCatalog_2
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(e.PropertyName, nameof(PathSettingsViewModel.DestinationPath), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
UpdateAiCsvOutputPathForDestination();
|
||||||
|
}
|
||||||
|
|
||||||
NotifyPropertyChanged(e.PropertyName);
|
NotifyPropertyChanged(e.PropertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2403,6 +2419,103 @@ namespace ImageCatalog_2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> ConfirmAiCsvOverwriteIfNeededAsync()
|
||||||
|
{
|
||||||
|
var csvOutputPath = NormalizeFilePathArgument(CsvOutputPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(csvOutputPath) || !File.Exists(csvOutputPath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmOverwrite = ConfirmAiCsvOverwriteAsync;
|
||||||
|
if (confirmOverwrite is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = $"Il file CSV esiste gia:\n{csvOutputPath}\n\nVuoi sovrascriverlo? Se scegli Annulla l'operazione OCR non verra avviata.";
|
||||||
|
return await confirmOverwrite(AiCsvOverwriteDialogTitle, message).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UpdateAiCsvOutputPathForDestination()
|
||||||
|
{
|
||||||
|
var updatedPath = BuildAiCsvOutputPathForDestination(CsvOutputPath, DestinationPath);
|
||||||
|
if (string.Equals(updatedPath, CsvOutputPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CsvOutputPath = updatedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string BuildAiCsvOutputPathForDestination(string currentCsvOutputPath, string destinationPath)
|
||||||
|
{
|
||||||
|
var normalizedCsvPath = NormalizeFilePathArgument(currentCsvOutputPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedCsvPath))
|
||||||
|
{
|
||||||
|
return currentCsvOutputPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(normalizedCsvPath);
|
||||||
|
var destinationFolderName = GetDestinationFolderName(destinationPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(destinationFolderName))
|
||||||
|
{
|
||||||
|
return normalizedCsvPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = Path.GetExtension(normalizedCsvPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(extension))
|
||||||
|
{
|
||||||
|
extension = ".csv";
|
||||||
|
}
|
||||||
|
|
||||||
|
var safeFileName = SanitizeFileName(destinationFolderName) + extension;
|
||||||
|
return string.IsNullOrWhiteSpace(directory)
|
||||||
|
? safeFileName
|
||||||
|
: Path.Combine(directory, safeFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDestinationFolderName(string destinationPath)
|
||||||
|
{
|
||||||
|
var normalizedDestinationPath = NormalizeDirectoryPathArgument(destinationPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedDestinationPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmedPath = normalizedDestinationPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmedPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootPath = Path.GetPathRoot(trimmedPath)?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
if (!string.IsNullOrWhiteSpace(rootPath)
|
||||||
|
&& string.Equals(trimmedPath, rootPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileName(trimmedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidFileNameChars = Path.GetInvalidFileNameChars();
|
||||||
|
var builder = new StringBuilder(value.Length);
|
||||||
|
foreach (var character in value)
|
||||||
|
{
|
||||||
|
builder.Append(Array.IndexOf(invalidFileNameChars, character) >= 0 ? '_' : character);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private void SetUseFaceGpu(bool value)
|
private void SetUseFaceGpu(bool value)
|
||||||
{
|
{
|
||||||
var currentValue = _ai.UseFaceGpu;
|
var currentValue = _ai.UseFaceGpu;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue