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}");
|
||||
}
|
||||
|
||||
[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]
|
||||
public void AiChildChange_RaisesDataModelPropertyChanged()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public partial class AvaloniaMainWindow : Window
|
|||
|
||||
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
||||
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
||||
_model.ConfirmAiCsvOverwriteAsync = ShowConfirmationDialogAsync;
|
||||
|
||||
_model.SelectSourceFolderRequested += async (_, _) =>
|
||||
{
|
||||
|
|
@ -283,6 +284,25 @@ public partial class AvaloniaMainWindow : Window
|
|||
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)
|
||||
{
|
||||
var layout = new StackPanel
|
||||
|
|
@ -310,4 +330,46 @@ public partial class AvaloniaMainWindow : Window
|
|||
layout.Children.Add(closeButton);
|
||||
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 MaddoShared;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace ImageCatalog_2
|
||||
{
|
||||
|
|
@ -82,6 +83,8 @@ namespace ImageCatalog_2
|
|||
|
||||
private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary);
|
||||
|
||||
private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente";
|
||||
|
||||
// ComboBox collections
|
||||
public List<string> AvailableFonts { get; }
|
||||
public List<string> VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" };
|
||||
|
|
@ -145,6 +148,12 @@ namespace ImageCatalog_2
|
|||
|
||||
private async Task StartAiAsync()
|
||||
{
|
||||
if (!await ConfirmAiCsvOverwriteIfNeededAsync().ConfigureAwait(false))
|
||||
{
|
||||
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MainToken = new CancellationTokenSource();
|
||||
try
|
||||
{
|
||||
|
|
@ -267,6 +276,8 @@ namespace ImageCatalog_2
|
|||
set => _ai.CsvOutputPath = value;
|
||||
}
|
||||
|
||||
public Func<string, string, Task<bool>>? ConfirmAiCsvOverwriteAsync { get; set; }
|
||||
|
||||
public bool UseNumberAiGpu
|
||||
{
|
||||
get => _ai.UseNumberAiGpu;
|
||||
|
|
@ -641,6 +652,11 @@ namespace ImageCatalog_2
|
|||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(e.PropertyName, nameof(PathSettingsViewModel.DestinationPath), StringComparison.Ordinal))
|
||||
{
|
||||
UpdateAiCsvOutputPathForDestination();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var currentValue = _ai.UseFaceGpu;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue