feat: Add AI extraction service and related view models
- Introduced `IAiExtractionService` and its implementation `AiExtractionService` for processing images and extracting text. - Created `AiResultItem` model to hold results from AI extraction. - Added `ImageProcessingCoordinator` to manage image processing tasks and provide progress updates. - Implemented view models for AI settings, path settings, processing state, race upload settings, and visual settings to support UI binding. - Updated `Program.cs` to register new services and dependencies. - Modified project file to skip MinVer execution during local builds.
This commit is contained in:
parent
bdf503c627
commit
3c722a66df
16 changed files with 1462 additions and 628 deletions
174
MaddoShared.Tests/DataModelCharacterizationTests.cs
Normal file
174
MaddoShared.Tests/DataModelCharacterizationTests.cs
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using ImageCatalog_2;
|
||||||
|
using ImageCatalog_2.Services;
|
||||||
|
using MaddoShared;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace MaddoShared.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class DataModelCharacterizationTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void SelectSourceFolderCommand_RaisesEvent()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
var raised = false;
|
||||||
|
model.SelectSourceFolderRequested += (_, _) => raised = true;
|
||||||
|
|
||||||
|
model.SelectSourceFolderCommand.Execute(null);
|
||||||
|
|
||||||
|
raised.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task SaveSettingsToFileAsync_DelegatesToSettingsService()
|
||||||
|
{
|
||||||
|
var settingsService = new Mock<ISettingsService>();
|
||||||
|
settingsService
|
||||||
|
.Setup(s => s.SaveSettingsAsync(It.IsAny<string>(), It.IsAny<object>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var model = CreateModel(settingsService: settingsService.Object);
|
||||||
|
|
||||||
|
await model.SaveSettingsToFileAsync("settings.xml");
|
||||||
|
|
||||||
|
settingsService.Verify(
|
||||||
|
s => s.SaveSettingsAsync("settings.xml", model),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task LoadSettingsFromFileAsync_DelegatesToSettingsService()
|
||||||
|
{
|
||||||
|
var settingsService = new Mock<ISettingsService>();
|
||||||
|
settingsService
|
||||||
|
.Setup(s => s.LoadSettingsAsync(It.IsAny<string>(), It.IsAny<object>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var model = CreateModel(settingsService: settingsService.Object);
|
||||||
|
|
||||||
|
await model.LoadSettingsFromFileAsync("settings.xml");
|
||||||
|
|
||||||
|
settingsService.Verify(
|
||||||
|
s => s.LoadSettingsAsync("settings.xml", model),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ThumbnailOptionIndex_UpdatesAuthoritativeThumbnailState()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
|
||||||
|
model.ThumbnailOptionIndex = (int)DataModel.ThumbnailOptionEnum.RaceTime;
|
||||||
|
|
||||||
|
model.ThumbnailOption.Should().Be(DataModel.ThumbnailOptionEnum.RaceTime);
|
||||||
|
model.AddRaceTimeToThumbnails.Should().BeTrue();
|
||||||
|
model.ThumbnailMode.Should().Be("RaceTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ProcessingChildChange_RaisesDataModelPropertyChanged()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
string? changed = null;
|
||||||
|
model.PropertyChanged += (_, args) => changed = args.PropertyName;
|
||||||
|
|
||||||
|
model.Processing.SpeedCounter = "12.00 f/s";
|
||||||
|
|
||||||
|
changed.Should().Be(nameof(DataModel.SpeedCounter));
|
||||||
|
model.SpeedCounter.Should().Be("12.00 f/s");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PathsNormalize_UpdatesFlattenedSourceAndDestination()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
model.SourcePath = "\"C:/input\"";
|
||||||
|
model.DestinationPath = "C:/output";
|
||||||
|
|
||||||
|
model.Paths.NormalizePaths();
|
||||||
|
|
||||||
|
model.SourcePath.Should().Be($"C:{System.IO.Path.DirectorySeparatorChar}input{System.IO.Path.DirectorySeparatorChar}");
|
||||||
|
model.DestinationPath.Should().Be($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void AiChildChange_RaisesDataModelPropertyChanged()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
string? changed = null;
|
||||||
|
model.PropertyChanged += (_, args) => changed = args.PropertyName;
|
||||||
|
|
||||||
|
model.Ai.ModelsFolderPath = "K:/models";
|
||||||
|
|
||||||
|
changed.Should().Be(nameof(DataModel.ModelsFolderPath));
|
||||||
|
model.ModelsFolderPath.Should().Be("K:/models");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void RaceUploadChildChange_RaisesDataModelPropertyChanged()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
string? changed = null;
|
||||||
|
model.PropertyChanged += (_, args) => changed = args.PropertyName;
|
||||||
|
|
||||||
|
model.RaceUpload.ApiLogin = "admin";
|
||||||
|
|
||||||
|
changed.Should().Be(nameof(DataModel.ApiLogin));
|
||||||
|
model.ApiLogin.Should().Be("admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void VisualChildChange_RaisesDataModelPropertyChanged()
|
||||||
|
{
|
||||||
|
var model = CreateModel();
|
||||||
|
string? changed = null;
|
||||||
|
model.PropertyChanged += (_, args) => changed = args.PropertyName;
|
||||||
|
|
||||||
|
model.Visual.FontSize = 42;
|
||||||
|
|
||||||
|
changed.Should().Be(nameof(DataModel.FontSize));
|
||||||
|
model.FontSize.Should().Be(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DataModel CreateModel(
|
||||||
|
ISettingsService? settingsService = null,
|
||||||
|
ITestService? testService = null)
|
||||||
|
{
|
||||||
|
var mapper = new Mock<AutoMapper.IMapper>().Object;
|
||||||
|
var picSettings = new PicSettings();
|
||||||
|
|
||||||
|
var imageCreator = new Mock<IImageCreator>();
|
||||||
|
imageCreator
|
||||||
|
.Setup(x => x.CreateImageAsync(It.IsAny<ImageState>(), It.IsAny<byte[]?>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var imageCreationService = new ImageCreationService(
|
||||||
|
new Mock<ILogger<ImageCreationService>>().Object,
|
||||||
|
picSettings,
|
||||||
|
imageCreator.Object);
|
||||||
|
|
||||||
|
var imageProcessingCoordinator = new ImageProcessingCoordinator(
|
||||||
|
imageCreationService,
|
||||||
|
new Mock<ILogger<ImageProcessingCoordinator>>().Object);
|
||||||
|
|
||||||
|
var aiExtractionService = new AiExtractionService(
|
||||||
|
new Mock<ILogger<AiExtractionService>>().Object);
|
||||||
|
|
||||||
|
return new DataModel(
|
||||||
|
testService ?? new Mock<ITestService>().Object,
|
||||||
|
settingsService ?? new Mock<ISettingsService>().Object,
|
||||||
|
imageCreationService,
|
||||||
|
aiExtractionService,
|
||||||
|
imageProcessingCoordinator,
|
||||||
|
picSettings,
|
||||||
|
mapper,
|
||||||
|
new Mock<ILogger<DataModel>>().Object,
|
||||||
|
versionProvider: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\imagecatalog\ImageCatalog 2.csproj" />
|
||||||
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,53 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
|
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
|
||||||
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
|
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
|
||||||
<ScrollViewer>
|
<Grid>
|
||||||
<StackPanel Margin="4">
|
<Grid.RowDefinitions>
|
||||||
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
<RowDefinition Height="Auto" />
|
||||||
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,6,0,0" />
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||||
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
<StackPanel Margin="4">
|
||||||
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
||||||
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,6,0,0" />
|
||||||
<Button Width="88" Margin="6,0,0,0" Command="{Binding SelectModelsFolderCommand}"
|
|
||||||
Grid.Column="2" Content="Scegli..." />
|
|
||||||
<Button Width="56" Margin="6,0,0,0" Grid.Column="3"
|
|
||||||
Click="OpenModelsFolder_Click" Content="Apri" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
|
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||||
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||||
<Button Width="88" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
|
<Button Width="88" Margin="6,0,0,0" Command="{Binding SelectModelsFolderCommand}"
|
||||||
Grid.Column="2" Content="Scegli..." />
|
Grid.Column="2" Content="Scegli..." />
|
||||||
<Button Width="56" Margin="6,0,0,0" Grid.Column="3"
|
<Button Width="56" Margin="6,0,0,0" Grid.Column="3"
|
||||||
Click="OpenCsvOutputFolder_Click" Content="Apri" />
|
Click="OpenModelsFolder_Click" Content="Apri" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,8,0,0" />
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
|
||||||
<avaloniaDataGrid:DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
|
<Button Content="Avvia AI" Command="{Binding StartAiCommand}" Width="120" />
|
||||||
AutoGenerateColumns="False" Height="200" Margin="0,4,0,0">
|
<Button Content="Annulla" Command="{Binding AsyncCancelOperationCommand}" Width="120" />
|
||||||
<avaloniaDataGrid:DataGrid.Columns>
|
</StackPanel>
|
||||||
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
|
||||||
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
</avaloniaDataGrid:DataGrid.Columns>
|
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
</avaloniaDataGrid:DataGrid>
|
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||||
</StackPanel>
|
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||||
</ScrollViewer>
|
<Button Width="88" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
|
||||||
|
Grid.Column="2" Content="Scegli..." />
|
||||||
|
<Button Width="56" Margin="6,0,0,0" Grid.Column="3"
|
||||||
|
Click="OpenCsvOutputFolder_Click" Content="Apri" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
|
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,4,0,4" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<avaloniaDataGrid:DataGrid Grid.Row="1" ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
|
||||||
|
AutoGenerateColumns="False" Margin="4,4,4,4" CanUserResizeColumns="True" VerticalAlignment="Stretch">
|
||||||
|
<avaloniaDataGrid:DataGrid.Columns>
|
||||||
|
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
||||||
|
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
||||||
|
</avaloniaDataGrid:DataGrid.Columns>
|
||||||
|
</avaloniaDataGrid:DataGrid>
|
||||||
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -25,6 +25,8 @@
|
||||||
This prevents MinVer from injecting a computed version into generated BAML/pack URIs which can cause
|
This prevents MinVer from injecting a computed version into generated BAML/pack URIs which can cause
|
||||||
WPF to try loading a mismatched assembly identity at runtime. Do a full clean rebuild after this change. -->
|
WPF to try loading a mismatched assembly identity at runtime. Do a full clean rebuild after this change. -->
|
||||||
<UpdateVersionProperties>true</UpdateVersionProperties>
|
<UpdateVersionProperties>true</UpdateVersionProperties>
|
||||||
|
<!-- Skip MinVer execution during local builds to avoid environment/runtime-specific failures. -->
|
||||||
|
<MinVerSkip>true</MinVerSkip>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
|
|
|
||||||
7
imagecatalog/Models/AiResultItem.cs
Normal file
7
imagecatalog/Models/AiResultItem.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace ImageCatalog_2.Models;
|
||||||
|
|
||||||
|
public class AiResultItem
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -151,14 +151,18 @@ static class Program
|
||||||
var testService = sp.GetRequiredService<ITestService>();
|
var testService = sp.GetRequiredService<ITestService>();
|
||||||
var settingsService = sp.GetRequiredService<ISettingsService>();
|
var settingsService = sp.GetRequiredService<ISettingsService>();
|
||||||
var imageCreation = sp.GetRequiredService<ImageCreationService>();
|
var imageCreation = sp.GetRequiredService<ImageCreationService>();
|
||||||
|
var aiExtractionService = sp.GetRequiredService<IAiExtractionService>();
|
||||||
|
var imageProcessingCoordinator = sp.GetRequiredService<IImageProcessingCoordinator>();
|
||||||
var picSettings = sp.GetRequiredService<PicSettings>();
|
var picSettings = sp.GetRequiredService<PicSettings>();
|
||||||
var mapper = sp.GetRequiredService<IMapper>();
|
var mapper = sp.GetRequiredService<IMapper>();
|
||||||
var logger = sp.GetRequiredService<ILogger<DataModel>>();
|
var logger = sp.GetRequiredService<ILogger<DataModel>>();
|
||||||
var versionProvider = sp.GetService<MaddoShared.IVersionProvider>();
|
var versionProvider = sp.GetService<MaddoShared.IVersionProvider>();
|
||||||
|
|
||||||
return new DataModel(testService, settingsService, imageCreation, picSettings, mapper, logger, versionProvider);
|
return new DataModel(testService, settingsService, imageCreation, aiExtractionService, imageProcessingCoordinator, picSettings, mapper, logger, versionProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddTransient<IAiExtractionService, AiExtractionService>();
|
||||||
|
services.AddTransient<IImageProcessingCoordinator, ImageProcessingCoordinator>();
|
||||||
services.AddTransient<ImageCreationService>();
|
services.AddTransient<ImageCreationService>();
|
||||||
#if WINDOWS
|
#if WINDOWS
|
||||||
services.AddTransient<ImageCreatorGDI>();
|
services.AddTransient<ImageCreatorGDI>();
|
||||||
|
|
|
||||||
132
imagecatalog/Services/AiExtractionService.cs
Normal file
132
imagecatalog/Services/AiExtractionService.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ImageCatalog_2.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.Services;
|
||||||
|
|
||||||
|
public class AiExtractionService : IAiExtractionService
|
||||||
|
{
|
||||||
|
private readonly ILogger<AiExtractionService> _logger;
|
||||||
|
|
||||||
|
public AiExtractionService(ILogger<AiExtractionService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync(
|
||||||
|
AiExtractionRequest request,
|
||||||
|
CancellationToken token,
|
||||||
|
Func<AiResultItem, Task> onResult,
|
||||||
|
Func<double, Task> onProgress)
|
||||||
|
{
|
||||||
|
var searchOption = request.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||||
|
|
||||||
|
var imageFiles = Directory.EnumerateFiles(request.SearchRoot, "*.*", searchOption)
|
||||||
|
.Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (imageFiles.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var extractedResults = new List<AiResultItem>();
|
||||||
|
|
||||||
|
Type? aiProcessorType = null;
|
||||||
|
object? aiProcessor = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(a => a.GetName().Name?.Equals("AIFotoONLUS.Core", StringComparison.OrdinalIgnoreCase) == true);
|
||||||
|
if (assembly != null)
|
||||||
|
{
|
||||||
|
aiProcessorType = assembly.GetType("AIFotoONLUS.Core.AiProcessor");
|
||||||
|
if (aiProcessorType != null)
|
||||||
|
{
|
||||||
|
aiProcessor = Activator.CreateInstance(aiProcessorType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "AIFotoONLUS.Core not available or failed to load via reflection");
|
||||||
|
}
|
||||||
|
|
||||||
|
var processed = 0;
|
||||||
|
var total = imageFiles.Count;
|
||||||
|
|
||||||
|
foreach (var file in imageFiles)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var extracted = string.Empty;
|
||||||
|
|
||||||
|
if (aiProcessorType is not null && aiProcessor is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = aiProcessorType.GetMethod("ExtractNumbersFromImage")
|
||||||
|
?? aiProcessorType.GetMethod("ExtractTextFromImage");
|
||||||
|
if (method is not null)
|
||||||
|
{
|
||||||
|
var value = method.Invoke(aiProcessor, new object[] { file });
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
extracted = value.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error invoking AI processor for {File}", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(extracted))
|
||||||
|
{
|
||||||
|
var result = new AiResultItem { Path = file, Text = extracted };
|
||||||
|
extractedResults.Add(result);
|
||||||
|
await onResult(result).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
var percent = total > 0 ? (processed * 100.0 / total) : 100.0;
|
||||||
|
await onProgress(percent).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(request.CsvOutputPath) ?? string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var sw = new StreamWriter(request.CsvOutputPath, false, Encoding.UTF8);
|
||||||
|
sw.WriteLine("Path,Text");
|
||||||
|
foreach (var r in extractedResults)
|
||||||
|
{
|
||||||
|
var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\"");
|
||||||
|
sw.WriteLine($"\"{r.Path}\",\"{safeText}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", request.CsvOutputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
imagecatalog/Services/IAiExtractionService.cs
Normal file
22
imagecatalog/Services/IAiExtractionService.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ImageCatalog_2.Models;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.Services;
|
||||||
|
|
||||||
|
public sealed class AiExtractionRequest
|
||||||
|
{
|
||||||
|
public required string SearchRoot { get; init; }
|
||||||
|
public required bool Recursive { get; init; }
|
||||||
|
public string CsvOutputPath { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAiExtractionService
|
||||||
|
{
|
||||||
|
Task RunAsync(
|
||||||
|
AiExtractionRequest request,
|
||||||
|
CancellationToken token,
|
||||||
|
Func<AiResultItem, Task> onResult,
|
||||||
|
Func<double, Task> onProgress);
|
||||||
|
}
|
||||||
28
imagecatalog/Services/IImageProcessingCoordinator.cs
Normal file
28
imagecatalog/Services/IImageProcessingCoordinator.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MaddoShared;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.Services
|
||||||
|
{
|
||||||
|
public readonly record struct ImageProcessedUpdate(string Status, int Total, int Processed);
|
||||||
|
|
||||||
|
public sealed class ImageProcessingRunRequest
|
||||||
|
{
|
||||||
|
public required ImageCreationService.Options Options { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ImageProcessingRunResult
|
||||||
|
{
|
||||||
|
public required string FinalSpeedCounter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IImageProcessingCoordinator
|
||||||
|
{
|
||||||
|
Task<ImageProcessingRunResult> RunAsync(
|
||||||
|
ImageProcessingRunRequest request,
|
||||||
|
CancellationToken token,
|
||||||
|
Action<ImageProcessedUpdate> onImageProcessed,
|
||||||
|
Action<string> onSpeedUpdated);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
imagecatalog/Services/ImageProcessingCoordinator.cs
Normal file
127
imagecatalog/Services/ImageProcessingCoordinator.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MaddoShared;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.Services
|
||||||
|
{
|
||||||
|
public class ImageProcessingCoordinator : IImageProcessingCoordinator
|
||||||
|
{
|
||||||
|
private readonly ImageCreationService _imageCreationService;
|
||||||
|
private readonly ILogger<ImageProcessingCoordinator> _logger;
|
||||||
|
|
||||||
|
[CLSCompliant(false)]
|
||||||
|
public ImageProcessingCoordinator(
|
||||||
|
ImageCreationService imageCreationService,
|
||||||
|
ILogger<ImageProcessingCoordinator> logger)
|
||||||
|
{
|
||||||
|
_imageCreationService = imageCreationService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImageProcessingRunResult> RunAsync(
|
||||||
|
ImageProcessingRunRequest request,
|
||||||
|
CancellationToken token,
|
||||||
|
Action<ImageProcessedUpdate> onImageProcessed,
|
||||||
|
Action<string> onSpeedUpdated)
|
||||||
|
{
|
||||||
|
var results = new ConcurrentBag<string>();
|
||||||
|
var recentDiffs = new Queue<int>();
|
||||||
|
const int recentWindowSize = 5;
|
||||||
|
|
||||||
|
int currentAmount = 0;
|
||||||
|
int previousAmount = 0;
|
||||||
|
int processedAtomic = 0;
|
||||||
|
|
||||||
|
var speedWatch = Stopwatch.StartNew();
|
||||||
|
using var speedTimer = new System.Threading.Timer(_ =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
previousAmount = currentAmount;
|
||||||
|
currentAmount = Volatile.Read(ref processedAtomic);
|
||||||
|
int diff = currentAmount - previousAmount;
|
||||||
|
if (diff < 0)
|
||||||
|
{
|
||||||
|
diff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (recentDiffs)
|
||||||
|
{
|
||||||
|
recentDiffs.Enqueue(diff);
|
||||||
|
if (recentDiffs.Count > recentWindowSize)
|
||||||
|
{
|
||||||
|
recentDiffs.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double avgRecent;
|
||||||
|
lock (recentDiffs)
|
||||||
|
{
|
||||||
|
avgRecent = recentDiffs.Count == 0 ? 0.0 : recentDiffs.Average();
|
||||||
|
}
|
||||||
|
|
||||||
|
double overall = 0.0;
|
||||||
|
if (speedWatch.Elapsed.TotalSeconds >= 1)
|
||||||
|
{
|
||||||
|
var elapsedSeconds = speedWatch.Elapsed.TotalSeconds;
|
||||||
|
var total = Volatile.Read(ref processedAtomic);
|
||||||
|
overall = elapsedSeconds > 0 ? total / elapsedSeconds : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var recentPerMin = avgRecent * 60.0;
|
||||||
|
var elapsed = speedWatch.Elapsed;
|
||||||
|
int hours = (int)elapsed.TotalHours;
|
||||||
|
int minutes = elapsed.Minutes;
|
||||||
|
int seconds = elapsed.Seconds;
|
||||||
|
var elapsedStr = $"{hours}h {minutes}m {seconds}s";
|
||||||
|
|
||||||
|
var speedText = $"{avgRecent:0.00} f/s (media: {overall:0.00} f/s) - {elapsedStr}{Environment.NewLine}media: {recentPerMin:0.00} f/m";
|
||||||
|
onSpeedUpdated(speedText);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to update speed counter");
|
||||||
|
}
|
||||||
|
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
EventHandler<Tuple<string, int>> onImageProcessedInternal = (_, args) =>
|
||||||
|
{
|
||||||
|
var processed = Interlocked.Increment(ref processedAtomic);
|
||||||
|
onImageProcessed(new ImageProcessedUpdate(args.Item1, args.Item2, processed));
|
||||||
|
};
|
||||||
|
|
||||||
|
await _imageCreationService.CreaCatalogoParallel(
|
||||||
|
request.Options,
|
||||||
|
results,
|
||||||
|
onImageProcessedInternal,
|
||||||
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
speedWatch.Stop();
|
||||||
|
|
||||||
|
var finalProcessed = Volatile.Read(ref processedAtomic);
|
||||||
|
double overallAvg = 0.0;
|
||||||
|
double overallPerMin = 0.0;
|
||||||
|
if (speedWatch.Elapsed.TotalSeconds > 0.0)
|
||||||
|
{
|
||||||
|
overallAvg = finalProcessed / speedWatch.Elapsed.TotalSeconds;
|
||||||
|
overallPerMin = overallAvg * 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalElapsed = speedWatch.Elapsed;
|
||||||
|
int finalHours = (int)finalElapsed.TotalHours;
|
||||||
|
int finalMinutes = finalElapsed.Minutes;
|
||||||
|
int finalSeconds = finalElapsed.Seconds;
|
||||||
|
|
||||||
|
return new ImageProcessingRunResult
|
||||||
|
{
|
||||||
|
FinalSpeedCounter = $"{finalHours}h {finalMinutes}m {finalSeconds}s{Environment.NewLine}media: {overallAvg:0.00} f/s{Environment.NewLine}media: {overallPerMin:0.00} f/m"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
imagecatalog/ViewModels/AiSettingsViewModel.cs
Normal file
75
imagecatalog/ViewModels/AiSettingsViewModel.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using ImageCatalog_2.Models;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.ViewModels;
|
||||||
|
|
||||||
|
public class AiSettingsViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private bool _extractNumbers;
|
||||||
|
public bool ExtractNumbers
|
||||||
|
{
|
||||||
|
get => _extractNumbers;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_extractNumbers = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _modelsFolderPath = string.Empty;
|
||||||
|
public string ModelsFolderPath
|
||||||
|
{
|
||||||
|
get => _modelsFolderPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_modelsFolderPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _csvOutputPath = string.Empty;
|
||||||
|
public string CsvOutputPath
|
||||||
|
{
|
||||||
|
get => _csvOutputPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_csvOutputPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceExecutablePath = string.Empty;
|
||||||
|
public string FaceExecutablePath
|
||||||
|
{
|
||||||
|
get => _faceExecutablePath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceExecutablePath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _faceOutputFolderPath = string.Empty;
|
||||||
|
public string FaceOutputFolderPath
|
||||||
|
{
|
||||||
|
get => _faceOutputFolderPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_faceOutputFolderPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _aiProgress;
|
||||||
|
public double AiProgress
|
||||||
|
{
|
||||||
|
get => _aiProgress;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_aiProgress = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<AiResultItem> PreviewResults { get; } = new();
|
||||||
|
}
|
||||||
49
imagecatalog/ViewModels/PathSettingsViewModel.cs
Normal file
49
imagecatalog/ViewModels/PathSettingsViewModel.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.ViewModels;
|
||||||
|
|
||||||
|
public class PathSettingsViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _sourcePath = string.Empty;
|
||||||
|
public string SourcePath
|
||||||
|
{
|
||||||
|
get => _sourcePath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_sourcePath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _destinationPath = string.Empty;
|
||||||
|
public string DestinationPath
|
||||||
|
{
|
||||||
|
get => _destinationPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_destinationPath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NormalizePaths()
|
||||||
|
{
|
||||||
|
SourcePath = NormalizePath(SourcePath);
|
||||||
|
DestinationPath = NormalizePath(DestinationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path.Trim().Trim('"');
|
||||||
|
path = path.Replace('/', Path.DirectorySeparatorChar)
|
||||||
|
.Replace('\\', Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
imagecatalog/ViewModels/ProcessingStateViewModel.cs
Normal file
82
imagecatalog/ViewModels/ProcessingStateViewModel.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.ViewModels;
|
||||||
|
|
||||||
|
public class ProcessingStateViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _processingStatus = string.Empty;
|
||||||
|
public string ProcessingStatus
|
||||||
|
{
|
||||||
|
get => _processingStatus;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_processingStatus = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _processedImagesCount;
|
||||||
|
public int ProcessedImagesCount
|
||||||
|
{
|
||||||
|
get => _processedImagesCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_processedImagesCount = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _totalImagesCount;
|
||||||
|
public int TotalImagesCount
|
||||||
|
{
|
||||||
|
get => _totalImagesCount;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_totalImagesCount = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _progressBarValue;
|
||||||
|
public int ProgressBarValue
|
||||||
|
{
|
||||||
|
get => _progressBarValue;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_progressBarValue = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _progressBarMaximum = 100;
|
||||||
|
public int ProgressBarMaximum
|
||||||
|
{
|
||||||
|
get => _progressBarMaximum;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_progressBarMaximum = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _speedCounter = "-";
|
||||||
|
public string SpeedCounter
|
||||||
|
{
|
||||||
|
get => _speedCounter;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_speedCounter = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetForRun()
|
||||||
|
{
|
||||||
|
ProcessingStatus = "Elaborazione in corso...";
|
||||||
|
TotalImagesCount = 0;
|
||||||
|
ProcessedImagesCount = 0;
|
||||||
|
SpeedCounter = "-f/s";
|
||||||
|
ProgressBarValue = 0;
|
||||||
|
ProgressBarMaximum = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
imagecatalog/ViewModels/RaceUploadSettingsViewModel.cs
Normal file
149
imagecatalog/ViewModels/RaceUploadSettingsViewModel.cs
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ImageCatalog_2.ViewModels;
|
||||||
|
|
||||||
|
public class RaceUploadSettingsViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _apiLogin = string.Empty;
|
||||||
|
public string ApiLogin
|
||||||
|
{
|
||||||
|
get => _apiLogin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiLogin = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiPassword = string.Empty;
|
||||||
|
public string ApiPassword
|
||||||
|
{
|
||||||
|
get => _apiPassword;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiPassword = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiRaceDescription = string.Empty;
|
||||||
|
public string ApiRaceDescription
|
||||||
|
{
|
||||||
|
get => _apiRaceDescription;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiRaceDescription = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiRaceTypeId = "1";
|
||||||
|
public string ApiRaceTypeId
|
||||||
|
{
|
||||||
|
get => _apiRaceTypeId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiRaceTypeId = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime _apiRaceStartDate = DateTime.Today;
|
||||||
|
public DateTime ApiRaceStartDate
|
||||||
|
{
|
||||||
|
get => _apiRaceStartDate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiRaceStartDate = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime _apiRaceEndDate = DateTime.Today;
|
||||||
|
public DateTime ApiRaceEndDate
|
||||||
|
{
|
||||||
|
get => _apiRaceEndDate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiRaceEndDate = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiPathBase = string.Empty;
|
||||||
|
public string ApiPathBase
|
||||||
|
{
|
||||||
|
get => _apiPathBase;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiPathBase = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiLocalita = string.Empty;
|
||||||
|
public string ApiLocalita
|
||||||
|
{
|
||||||
|
get => _apiLocalita;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiLocalita = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _apiEventoInLineaIndex;
|
||||||
|
public int ApiEventoInLineaIndex
|
||||||
|
{
|
||||||
|
get => _apiEventoInLineaIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiEventoInLineaIndex = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _apiTipoIndexValue = 1;
|
||||||
|
public int ApiTipoIndexValue
|
||||||
|
{
|
||||||
|
get => _apiTipoIndexValue;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiTipoIndexValue = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _apiFreeEventIndex;
|
||||||
|
public int ApiFreeEventIndex
|
||||||
|
{
|
||||||
|
get => _apiFreeEventIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiFreeEventIndex = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiRaceId = string.Empty;
|
||||||
|
public string ApiRaceId
|
||||||
|
{
|
||||||
|
get => _apiRaceId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiRaceId = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiRemoteProcessedBasePath = string.Empty;
|
||||||
|
public string ApiRemoteProcessedBasePath
|
||||||
|
{
|
||||||
|
get => _apiRemoteProcessedBasePath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_apiRemoteProcessedBasePath = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
312
imagecatalog/ViewModels/VisualSettingsViewModel.cs
Normal file
312
imagecatalog/ViewModels/VisualSettingsViewModel.cs
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
namespace ImageCatalog_2.ViewModels;
|
||||||
|
|
||||||
|
public class VisualSettingsViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private string _horizontalText = string.Empty;
|
||||||
|
public string HorizontalText
|
||||||
|
{
|
||||||
|
get => _horizontalText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_horizontalText = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _verticalText = string.Empty;
|
||||||
|
public string VerticalText
|
||||||
|
{
|
||||||
|
get => _verticalText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_verticalText = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _overwriteImages;
|
||||||
|
public bool OverwriteImages
|
||||||
|
{
|
||||||
|
get => _overwriteImages;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_overwriteImages = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _thumbnailPrefix = "tn_";
|
||||||
|
public string ThumbnailPrefix
|
||||||
|
{
|
||||||
|
get => _thumbnailPrefix;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_thumbnailPrefix = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _thumbnailHeight = 350;
|
||||||
|
public int ThumbnailHeight
|
||||||
|
{
|
||||||
|
get => _thumbnailHeight;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_thumbnailHeight = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _thumbnailWidth = 350;
|
||||||
|
public int ThumbnailWidth
|
||||||
|
{
|
||||||
|
get => _thumbnailWidth;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_thumbnailWidth = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _photoBigHeight = 2240;
|
||||||
|
public int PhotoBigHeight
|
||||||
|
{
|
||||||
|
get => _photoBigHeight;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_photoBigHeight = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _photoBigWidth = 2240;
|
||||||
|
public int PhotoBigWidth
|
||||||
|
{
|
||||||
|
get => _photoBigWidth;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_photoBigWidth = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _fontSize = 20;
|
||||||
|
public int FontSize
|
||||||
|
{
|
||||||
|
get => _fontSize;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fontSize = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _fontSizeThumbnail = 50;
|
||||||
|
public int FontSizeThumbnail
|
||||||
|
{
|
||||||
|
get => _fontSizeThumbnail;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fontSizeThumbnail = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _fontName = "Arial";
|
||||||
|
public string FontName
|
||||||
|
{
|
||||||
|
get => _fontName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fontName = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _fontBold;
|
||||||
|
public bool FontBold
|
||||||
|
{
|
||||||
|
get => _fontBold;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fontBold = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _textTransparency;
|
||||||
|
public int TextTransparency
|
||||||
|
{
|
||||||
|
get => _textTransparency;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_textTransparency = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _textMargin = 8;
|
||||||
|
public int TextMargin
|
||||||
|
{
|
||||||
|
get => _textMargin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_textMargin = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _textColorRgb = "Yellow";
|
||||||
|
public string TextColorRGB
|
||||||
|
{
|
||||||
|
get => _textColorRgb;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_textColorRgb = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _transparentColor = "#FFFFFF";
|
||||||
|
public string TransparentColor
|
||||||
|
{
|
||||||
|
get => _transparentColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_transparentColor = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _useTransparentColor;
|
||||||
|
public bool UseTransparentColor
|
||||||
|
{
|
||||||
|
get => _useTransparentColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_useTransparentColor = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _logoFile = string.Empty;
|
||||||
|
public string LogoFile
|
||||||
|
{
|
||||||
|
get => _logoFile;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logoFile = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _logoHeight = 430;
|
||||||
|
public int LogoHeight
|
||||||
|
{
|
||||||
|
get => _logoHeight;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logoHeight = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _logoWidth = 430;
|
||||||
|
public int LogoWidth
|
||||||
|
{
|
||||||
|
get => _logoWidth;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logoWidth = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _logoMargin = 290;
|
||||||
|
public int LogoMargin
|
||||||
|
{
|
||||||
|
get => _logoMargin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logoMargin = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _logoTransparency = 100;
|
||||||
|
public int LogoTransparency
|
||||||
|
{
|
||||||
|
get => _logoTransparency;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_logoTransparency = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _verticalTextSize = 20;
|
||||||
|
public int VerticalTextSize
|
||||||
|
{
|
||||||
|
get => _verticalTextSize;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_verticalTextSize = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _verticalTextMargin = 6;
|
||||||
|
public int VerticalTextMargin
|
||||||
|
{
|
||||||
|
get => _verticalTextMargin;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_verticalTextMargin = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _jpegQuality = 85;
|
||||||
|
public int JpegQuality
|
||||||
|
{
|
||||||
|
get => _jpegQuality;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_jpegQuality = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _jpegQualityThumbnail = 30;
|
||||||
|
public int JpegQualityThumbnail
|
||||||
|
{
|
||||||
|
get => _jpegQualityThumbnail;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_jpegQualityThumbnail = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _addLogo;
|
||||||
|
public bool AddLogo
|
||||||
|
{
|
||||||
|
get => _addLogo;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_addLogo = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _keepOriginalDimensions;
|
||||||
|
public bool KeepOriginalDimensions
|
||||||
|
{
|
||||||
|
get => _keepOriginalDimensions;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_keepOriginalDimensions = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue