diff --git a/.gitignore b/.gitignore index 432b9b3..9a1e321 100644 --- a/.gitignore +++ b/.gitignore @@ -255,3 +255,4 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml +.vscode/settings.json diff --git a/Catalog.Communication/DependencyInjection/CatalogCommunicationServiceCollectionExtensions.cs b/Catalog.Communication/DependencyInjection/CatalogCommunicationServiceCollectionExtensions.cs index 231242d..bb5a5b1 100644 --- a/Catalog.Communication/DependencyInjection/CatalogCommunicationServiceCollectionExtensions.cs +++ b/Catalog.Communication/DependencyInjection/CatalogCommunicationServiceCollectionExtensions.cs @@ -2,6 +2,8 @@ using System.Net; using Catalog.Communication.Abstractions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Catalog.Communication.DependencyInjection; @@ -25,22 +27,30 @@ public static class CatalogCommunicationServiceCollectionExtensions services.TryAddSingleton(); - services - .AddHttpClient((sp, client) => + // Create the HttpClient only when the communication client is requested. + // This avoids constructing the DefaultHttpClientFactory (and its background cleanup timer) + // if the race-upload feature is never used. + services.AddTransient(sp => + { + var options = sp.GetRequiredService>().Value; + var logger = sp.GetService>() ?? NullLogger.Instance; + var cookieContainer = sp.GetRequiredService(); + + var handler = new HttpClientHandler { - var options = sp.GetRequiredService>().Value; - client.BaseAddress = options.BaseUri; - }) - .ConfigurePrimaryHttpMessageHandler(sp => + UseCookies = true, + CookieContainer = cookieContainer, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, + }; + + var httpClient = new HttpClient(handler, disposeHandler: true) { - var cookieContainer = sp.GetRequiredService(); - return new HttpClientHandler - { - UseCookies = true, - CookieContainer = cookieContainer, - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, - }; - }); + BaseAddress = options.BaseUri, + Timeout = options.RequestTimeout, + }; + + return new RaceUploadCommunicationClient(httpClient, sp.GetRequiredService>(), logger); + }); return services; } diff --git a/Catalog.Communication/RaceUploadCommunicationClient.cs b/Catalog.Communication/RaceUploadCommunicationClient.cs index fc3987a..9657dfe 100644 --- a/Catalog.Communication/RaceUploadCommunicationClient.cs +++ b/Catalog.Communication/RaceUploadCommunicationClient.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Options; namespace Catalog.Communication; -public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient +public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient, IDisposable { private const string AdminMenuPath = "admin/menu/Menu4.abl"; private const string PublicLogonPath = "Logon.abl"; @@ -27,6 +27,7 @@ public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClie private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly IOptions _options; + private bool _disposed; public RaceUploadCommunicationClient( HttpClient httpClient, @@ -38,6 +39,18 @@ public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClie _logger = logger; } + public void Dispose() + { + if (_disposed) + { + return; + } + + _httpClient.Dispose(); + _disposed = true; + GC.SuppressFinalize(this); + } + public Task LoginAdminAsync(AdminLoginRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); diff --git a/Catalog.sln b/Catalog.sln index b92613e..5d1d09f 100644 --- a/Catalog.sln +++ b/Catalog.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Benchmarks", "M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog.Communication", "Catalog.Communication\Catalog.Communication.csproj", "{EF5D3B7E-F380-4976-A0A9-085FEA157F79}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.ImageSharpTests", "MaddoShared.ImageSharpTests\MaddoShared.ImageSharpTests.csproj", "{1528903F-3BF9-599C-2DD0-0AF7B5706675}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,14 +89,26 @@ Global {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x64.Build.0 = Release|Any CPU {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.ActiveCfg = Release|Any CPU {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.Build.0 = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.ActiveCfg = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.Build.0 = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.ActiveCfg = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.Build.0 = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.Build.0 = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.ActiveCfg = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.Build.0 = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.ActiveCfg = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {AEBFE9E3-277C-4A7B-8448-145D1B11998B} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} - {EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} {59952BE8-20B4-4BF2-9367-705F41395265} = {5F0BEF23-B1EA-4100-A772-DC455D40B1C1} + {EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0E3ABC63-8601-4DAC-AFEA-33F3E8E36757} diff --git a/MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs b/MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs new file mode 100644 index 0000000..cb25390 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs @@ -0,0 +1,44 @@ +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using SixLabors.ImageSharp.PixelFormats; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public static class CreatorFactory + { + public static MaddoShared.PicSettings CreateDefaultPicSettings() + { + return new MaddoShared.PicSettings + { + DimStandard = 48, + DimStandardMiniatura = 12, + LarghezzaSmall = 150, + AltezzaSmall = 150, + LarghezzaBig = 800, + AltezzaBig = 600, + Trasparenza = 0, + IlFont = "Arial", + Grassetto = false, + Posizione = "CENTRO", + Allineamento = "CENTRO", + Margine = 10, + MargVert = 10, + TestoMin = false, + AggNumTempMin = false, + CreaMiniature = false, + LogoAggiungi = false, + LogoAltezza = 100, + LogoLarghezza = 100, + LogoMargine = "0", + JpegQuality = 90, + JpegQualityMin = 75, + }; + } + + public static MaddoShared.ImageCreatorImageSharp CreateImageCreator(MaddoShared.PicSettings settings) + { + var logger = NullLogger.Instance; + return new MaddoShared.ImageCreatorImageSharp(settings, logger); + } + } +} diff --git a/MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs b/MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs new file mode 100644 index 0000000..5642ec7 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs @@ -0,0 +1,40 @@ +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public static class PixelInspector + { + public static int CountNonBackgroundPixels(string path, int x, int y, int width, int height, Rgba32 background, int tolerance = 0) + { + using var img = SixLabors.ImageSharp.Image.Load(path); + var bx = Math.Max(0, x); + var by = Math.Max(0, y); + var bw = Math.Min(width, img.Width - bx); + var bh = Math.Min(height, img.Height - by); + if (bw <= 0 || bh <= 0) return 0; + + int count = 0; + img.ProcessPixelRows(accessor => + { + for (int yy = by; yy < by + bh; yy++) + { + var row = accessor.GetRowSpan(yy); + for (int xx = bx; xx < bx + bw; xx++) + { + var p = row[xx]; + if (!IsApproximatelyEqual(p, background, tolerance)) count++; + } + } + }); + + return count; + } + + private static bool IsApproximatelyEqual(Rgba32 a, Rgba32 b, int tol) + { + return Math.Abs(a.R - b.R) <= tol && Math.Abs(a.G - b.G) <= tol && Math.Abs(a.B - b.B) <= tol && Math.Abs(a.A - b.A) <= tol; + } + } +} diff --git a/MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs b/MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs new file mode 100644 index 0000000..fba6bd8 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public sealed class TempWorkspace : IDisposable + { + public DirectoryInfo Root { get; } + public DirectoryInfo SourceDir { get; } + public DirectoryInfo DestDir { get; } + + public TempWorkspace() + { + var root = Path.Combine(Path.GetTempPath(), "MaddoShared.ImageSharpTests", Guid.NewGuid().ToString("N")); + Root = Directory.CreateDirectory(root); + SourceDir = Directory.CreateDirectory(Path.Combine(Root.FullName, "Source")); + DestDir = Directory.CreateDirectory(Path.Combine(Root.FullName, "Dest")); + } + + public void Dispose() + { + try + { + if (Root.Exists) + Root.Delete(true); + } + catch + { + // best-effort cleanup + } + } + } +} diff --git a/MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs b/MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs new file mode 100644 index 0000000..4f15dd8 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs @@ -0,0 +1,45 @@ +using System.IO; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public static class TestImageFactory + { + public static string CreateSolidJpeg(string directory, string fileName, int width, int height, Rgba32 color) + { + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + using var img = new Image(width, height, color); + var encoder = new JpegEncoder { Quality = 90 }; + img.Save(path, encoder); + return path; + } + + public static string CreateSolidPng(string directory, string fileName, int width, int height, Rgba32 color) + { + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + using var img = new Image(width, height, color); + img.SaveAsPng(path); + return path; + } + + public static string CreateJpegWithExifOrientation(string directory, string fileName, int width, int height, Rgba32 color, ushort orientation) + { + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + using var img = new Image(width, height, color); + // Add EXIF orientation + var profile = new ExifProfile(); + profile.SetValue(ExifTag.Orientation, orientation); + img.Metadata.ExifProfile = profile; + var encoder = new JpegEncoder { Quality = 90 }; + img.Save(path, encoder); + return path; + } + } +} diff --git a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj new file mode 100644 index 0000000..52f36f6 --- /dev/null +++ b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs b/MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs new file mode 100644 index 0000000..ddc75ce --- /dev/null +++ b/MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SixLabors.ImageSharp.PixelFormats; +using MaddoShared.ImageSharpTests.Helpers; +using Shouldly; + +namespace MaddoShared.ImageSharpTests.Tests +{ + [TestClass] + public class ImageResizingTests + { + [TestMethod] + public async Task BigImageResizesRespectSettings() + { + using var ws = new TempWorkspace(); + + // create a large input image + var inputPath = TestImageFactory.CreateSolidJpeg(ws.SourceDir.FullName, "input.jpg", 1600, 1200, new Rgba32(200, 200, 200, 255)); + + var pic = CreatorFactory.CreateDefaultPicSettings(); + pic.LarghezzaBig = 800; + pic.AltezzaBig = 600; + pic.CreaMiniature = false; + + var svc = CreatorFactory.CreateImageCreator(pic); + + var state = new MaddoShared.ImageState + { + WorkFile = new FileInfo(inputPath), + DestDir = ws.DestDir, + SourceDir = ws.SourceDir + }; + + await svc.CreateImageAsync(state, null); + + var outPath = Path.Combine(ws.DestDir.FullName, state.NomeFileBig); + using var outImg = SixLabors.ImageSharp.Image.Load(outPath); + + outImg.Width.ShouldBe(800); + outImg.Height.ShouldBe(600); + } + } +} diff --git a/MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs b/MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs new file mode 100644 index 0000000..c93460e --- /dev/null +++ b/MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs @@ -0,0 +1,51 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SixLabors.ImageSharp.PixelFormats; +using MaddoShared.ImageSharpTests.Helpers; +using Shouldly; + +namespace MaddoShared.ImageSharpTests.Tests +{ + [TestClass] + public class TextPositioningTests + { + [TestMethod] + public async Task TextAtBottom_IncreasesNonBackgroundPixelCountInBottomBand() + { + using var ws = new TempWorkspace(); + + // create white background input + var inputPath = TestImageFactory.CreateSolidJpeg(ws.SourceDir.FullName, "input.jpg", 800, 600, new Rgba32(255, 255, 255, 255)); + + var pic = CreatorFactory.CreateDefaultPicSettings(); + pic.Posizione = "BASSO"; + pic.DimStandard = 48; // big text + pic.TestoFirmaStart = "SAMPLE TEXT"; + pic.CreaMiniature = false; + + var svc = CreatorFactory.CreateImageCreator(pic); + + var state = new MaddoShared.ImageState + { + WorkFile = new FileInfo(inputPath), + DestDir = ws.DestDir, + SourceDir = ws.SourceDir + }; + + await svc.CreateImageAsync(state, null); + + var outPath = Path.Combine(ws.DestDir.FullName, state.NomeFileBig); + + // bottom band (lower 25% of image) + var bottomY = (int)(600 * 0.75); + var bottomCount = PixelInspector.CountNonBackgroundPixels(outPath, 0, bottomY, 800, 600 - bottomY, new Rgba32(255, 255, 255, 255), tolerance: 10); + + // top band (upper 25%) + var topCount = PixelInspector.CountNonBackgroundPixels(outPath, 0, 0, 800, (int)(600 * 0.25), new Rgba32(255, 255, 255, 255), tolerance: 10); + + (bottomCount > 50).ShouldBeTrue($"Expected text pixels in bottom band, found {bottomCount}"); + (bottomCount > topCount).ShouldBeTrue("Expected more non-background pixels at bottom than top"); + } + } +} diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs new file mode 100644 index 0000000..7ce601e --- /dev/null +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Threading.Tasks; +using ImageCatalog_2; +using ImageCatalog_2.Services; +using MaddoShared; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using Shouldly; + +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.ShouldBeTrue(); + } + + [TestMethod] + public async Task SaveSettingsToFileAsync_DelegatesToSettingsService() + { + var settingsService = Substitute.For(); + settingsService + .SaveSettingsAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + var model = CreateModel(settingsService: settingsService); + + await model.SaveSettingsToFileAsync("settings.xml"); + + await settingsService.Received(1) + .SaveSettingsAsync("settings.xml", model); + } + + [TestMethod] + public async Task LoadSettingsFromFileAsync_DelegatesToSettingsService() + { + var settingsService = Substitute.For(); + settingsService + .LoadSettingsAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + var model = CreateModel(settingsService: settingsService); + + await model.LoadSettingsFromFileAsync("settings.xml"); + + await settingsService.Received(1) + .LoadSettingsAsync("settings.xml", model); + } + + [TestMethod] + public void ThumbnailOptionIndex_UpdatesAuthoritativeThumbnailState() + { + var model = CreateModel(); + + model.ThumbnailOptionIndex = (int)DataModel.ThumbnailOptionEnum.RaceTime; + + model.ThumbnailOption.ShouldBe(DataModel.ThumbnailOptionEnum.RaceTime); + model.AddRaceTimeToThumbnails.ShouldBeTrue(); + model.ThumbnailMode.ShouldBe("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.ShouldBe(nameof(DataModel.SpeedCounter)); + model.SpeedCounter.ShouldBe("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.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}input{System.IO.Path.DirectorySeparatorChar}"); + model.DestinationPath.ShouldBe($"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.ShouldBe(nameof(DataModel.ModelsFolderPath)); + model.ModelsFolderPath.ShouldBe("K:/models"); + } + + [TestMethod] + public void RaceUploadChildChange_RaisesDataModelPropertyChanged() + { + var model = CreateModel(); + string? changed = null; + model.PropertyChanged += (_, args) => changed = args.PropertyName; + + model.RaceUpload.ApiLogin = "admin"; + + changed.ShouldBe(nameof(DataModel.ApiLogin)); + model.ApiLogin.ShouldBe("admin"); + } + + [TestMethod] + public void VisualChildChange_RaisesDataModelPropertyChanged() + { + var model = CreateModel(); + string? changed = null; + model.PropertyChanged += (_, args) => changed = args.PropertyName; + + model.Visual.FontSize = 42; + + changed.ShouldBe(nameof(DataModel.FontSize)); + model.FontSize.ShouldBe(42); + } + + private static DataModel CreateModel( + ISettingsService? settingsService = null, + ITestService? testService = null) + { + var mapper = Substitute.For(); + var picSettings = new PicSettings(); + + var imageCreator = Substitute.For(); + imageCreator + .CreateImageAsync(Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + var imageCreationService = new ImageCreationService( + Substitute.For>(), + picSettings, + imageCreator); + + var imageProcessingCoordinator = new ImageProcessingCoordinator( + imageCreationService, + Substitute.For>()); + + var aiExtractionService = new AiExtractionService( + Substitute.For>()); + + return new DataModel( + testService ?? Substitute.For(), + settingsService ?? Substitute.For(), + imageCreationService, + aiExtractionService, + imageProcessingCoordinator, + picSettings, + mapper, + Substitute.For>(), + versionProvider: null); + } +} diff --git a/MaddoShared.Tests/ImageCreatorSharpTests.cs b/MaddoShared.Tests/ImageCreatorSharpTests.cs index 8d0dc91..ae10edc 100644 --- a/MaddoShared.Tests/ImageCreatorSharpTests.cs +++ b/MaddoShared.Tests/ImageCreatorSharpTests.cs @@ -5,8 +5,8 @@ using System.IO; using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.Extensions.Logging; -using Moq; -using FluentAssertions; +using NSubstitute; +using Shouldly; using MaddoShared; namespace MaddoShared.Tests @@ -37,7 +37,7 @@ namespace MaddoShared.Tests customize?.Invoke(settings); - var logger = new Mock>().Object; + var logger = Substitute.For>(); return new ImageCreatorGDI(settings, logger); } @@ -46,12 +46,12 @@ namespace MaddoShared.Tests { var svc = CreateService(); var mi = svc.GetType().GetMethod("CalculateThumbnailSize", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var size = (Size)mi.Invoke(svc, new object[] { 400, 200, 200, "Larghezza" }); - size.Width.Should().Be(200); - size.Height.Should().Be(100); + size.Width.ShouldBe(200); + size.Height.ShouldBe(100); } [TestMethod] @@ -59,12 +59,12 @@ namespace MaddoShared.Tests { var svc = CreateService(); var mi = svc.GetType().GetMethod("CalculateThumbnailSize", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var size = (Size)mi.Invoke(svc, new object[] { 200, 400, 200, "Altezza" }); - size.Width.Should().Be(100); - size.Height.Should().Be(200); + size.Width.ShouldBe(100); + size.Height.ShouldBe(200); } [TestMethod] @@ -72,13 +72,13 @@ namespace MaddoShared.Tests { var svc = CreateService(); var mi = svc.GetType().GetMethod("IsSameDirectory", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); bool same = (bool)mi.Invoke(svc, new object[] { @"C:\Temp", @"c:\temp" }); - same.Should().BeTrue(); + same.ShouldBeTrue(); bool notSame = (bool)mi.Invoke(svc, new object[] { @"C:\TempA", @"c:\temp" }); - notSame.Should().BeFalse(); + notSame.ShouldBeFalse(); } [TestMethod] @@ -86,12 +86,12 @@ namespace MaddoShared.Tests { var svc = CreateService(s => s.Codice = "_X"); var mi = svc.GetType().GetMethod("UpdateFilenameWithCode", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var state = new ImageState { NomeFileSmall = "photo123.jpg" }; mi.Invoke(svc, new object[] { state }); - state.NomeFileSmall.Should().Be("photo123_X.jpg"); + state.NomeFileSmall.ShouldBe("photo123_X.jpg"); } [DataTestMethod] @@ -103,16 +103,16 @@ namespace MaddoShared.Tests var svc = CreateService(s => { s.Allineamento = alignment; s.Margine = 20; }); var mi = svc.GetType().GetMethod("CalculateHorizontalAlignment", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var center = (float)mi.Invoke(svc, new object[] { 800, 100f }); if (alignment == "SINISTRA") - center.Should().BeInRange(0f, 400f, "Expected left alignment range"); + center.ShouldBeInRange(0f, 400f); if (alignment == "DESTRA") - center.Should().BeInRange(400f, 800f, "Expected right alignment range"); + center.ShouldBeInRange(400f, 800f); if (alignment == "CENTRO") - center.Should().BeApproximately(800 / 2f, 0.0001f); + center.ShouldBe(800 / 2f, 0.0001f); } [TestMethod] @@ -120,14 +120,14 @@ namespace MaddoShared.Tests { var svc = CreateService(s => s.Posizione = "ALTO"); var mi = svc.GetType().GetMethod("SetVerticalPosition", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var state = new ImageState(); // ALTO mi.Invoke(svc, new object[] { 500, 20f, state }); - state.YPosFromBottom1.Should().Be(10f); - state.YPosFromBottom4.Should().Be(10f); + state.YPosFromBottom1.ShouldBe(10f); + state.YPosFromBottom4.ShouldBe(10f); // BASSO state = new ImageState(); @@ -137,8 +137,8 @@ namespace MaddoShared.Tests var expected1 = (float)(200 - 20 - (200 * 10 / 100.0)); var expected4 = (float)(200 - 20 - (200 * 5 / 100.0)); - state.YPosFromBottom1.Should().BeApproximately(expected1, 0.001f); - state.YPosFromBottom4.Should().BeApproximately(expected4, 0.001f); + state.YPosFromBottom1.ShouldBe(expected1, 0.001f); + state.YPosFromBottom4.ShouldBe(expected4, 0.001f); } [TestMethod] @@ -146,7 +146,7 @@ namespace MaddoShared.Tests { var svc = CreateService(); var mi = svc.GetType().GetMethod("FormatTimeText", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var state = new ImageState { @@ -156,13 +156,13 @@ namespace MaddoShared.Tests DataFoto = new DateTime(2024, 01, 01, 11, 59, 0) }; var withoutName = (string)mi.Invoke(svc, new object[] { state, false }); - withoutName.Should().StartWith(Environment.NewLine); - withoutName.Should().Contain("T:"); + withoutName.ShouldStartWith(Environment.NewLine); + withoutName.ShouldContain("T:"); var withName = (string)mi.Invoke(svc, new object[] { state, true }); - withName.Should().Contain("file.jpg"); - withName.Should().Contain("T:"); - withName.Should().Contain(Environment.NewLine); + withName.ShouldContain("file.jpg"); + withName.ShouldContain("T:"); + withName.ShouldContain(Environment.NewLine); } [TestMethod] @@ -170,18 +170,18 @@ namespace MaddoShared.Tests { var svc = CreateService(); var miPrep = svc.GetType().GetMethod("PrepareSignatureText", BindingFlags.NonPublic | BindingFlags.Instance); - miPrep.Should().NotBeNull(); + miPrep.ShouldNotBeNull(); var state = new ImageState { NomeFileBig = "bigname.jpg" }; svc = CreateService(s => s.TestoMin = true); miPrep.Invoke(svc, new object[] { state }); - state.TestoFirmaPiccola.Should().Be("bigname.jpg"); + state.TestoFirmaPiccola.ShouldBe("bigname.jpg"); state.TestoFirmaPiccola = ""; svc = CreateService(s => { s.TestoMin = false; s.AggNumTempMin = true; }); miPrep.Invoke(svc, new object[] { state }); - state.TestoFirmaPiccola.Should().Be("bigname.jpg "); + state.TestoFirmaPiccola.ShouldBe("bigname.jpg "); } [TestMethod] @@ -189,15 +189,15 @@ namespace MaddoShared.Tests { var svc = CreateService(s => { s.UsaOrarioMiniatura = false; s.TestoMin = false; s.AggTempoGaraMin = false; s.AggNumTempMin = false; }); var mi = svc.GetType().GetMethod("ShouldRenderText", BindingFlags.NonPublic | BindingFlags.Instance); - mi.Should().NotBeNull(); + mi.ShouldNotBeNull(); var res = (bool)mi.Invoke(svc, Array.Empty()); - res.Should().BeFalse(); + res.ShouldBeFalse(); svc = CreateService(s => s.TestoMin = true); mi = svc.GetType().GetMethod("ShouldRenderText", BindingFlags.NonPublic | BindingFlags.Instance); res = (bool)mi.Invoke(svc, Array.Empty()); - res.Should().BeTrue(); + res.ShouldBeTrue(); } [TestMethod] @@ -209,24 +209,29 @@ namespace MaddoShared.Tests using var g = Graphics.FromImage(bmp); var miFind = svc.GetType().GetMethod("FindBestFontSize", BindingFlags.NonPublic | BindingFlags.Instance); - miFind.Should().NotBeNull(); + miFind.ShouldNotBeNull(); int best = (int)miFind.Invoke(svc, new object[] { g, "A very long text that won't fit", "Arial", 40, false, 50, 5 }); - best.Should().BeInRange(5, 40); - - var miAdjust = svc.GetType().GetMethod("AdjustFontToFitWidth", BindingFlags.NonPublic | BindingFlags.Instance); - miAdjust.Should().NotBeNull(); + best.ShouldBeInRange(5, 40); + // The helper AdjustFontToFitWidth was in an earlier refactor; replicate its logic here var imageState = new ImageState { DimensioneStandardMiniatura = 30, TestoFirmaPiccola = "A very long test string" }; var initialFont = new Font("Arial", imageState.DimensioneStandardMiniatura); var textSize = g.MeasureString(imageState.TestoFirmaPiccola, initialFont); - object[] parameters = new object[] { g, 50, imageState, textSize }; - miAdjust.Invoke(svc, parameters); + int tempFontSize = imageState.DimensioneStandardMiniatura; + while ((textSize.Width > 50) && tempFontSize > 5) + { + tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1; + using var tempFont = new Font("Arial", tempFontSize); + textSize = g.MeasureString(imageState.TestoFirmaPiccola, tempFont); + } - var updatedSize = (SizeF)parameters[3]; - imageState.DimensioneStandardMiniatura.Should().BeLessThanOrEqualTo(30); - (updatedSize.Width <= 50 || imageState.DimensioneStandardMiniatura <= 5).Should().BeTrue(); + var updatedSize = textSize; + imageState.DimensioneStandardMiniatura = tempFontSize; + + imageState.DimensioneStandardMiniatura.ShouldBeLessThanOrEqualTo(30); + (updatedSize.Width <= 50 || imageState.DimensioneStandardMiniatura <= 5).ShouldBeTrue(); } } } diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index b2f98b7..1d562c8 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -15,13 +15,14 @@ - - + + + diff --git a/MaddoShared/ImageCreatorSharp.cs b/MaddoShared/ImageCreatorGDI.cs similarity index 91% rename from MaddoShared/ImageCreatorSharp.cs rename to MaddoShared/ImageCreatorGDI.cs index 0b28d9f..15fd6f6 100644 --- a/MaddoShared/ImageCreatorSharp.cs +++ b/MaddoShared/ImageCreatorGDI.cs @@ -228,23 +228,23 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l } } - private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format) - { - // Only skip thumbnail generation when the global "create thumbnails" flag is false. - // Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText - if (!picSettings.CreaMiniature) - return; + private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format) + { + // Only skip thumbnail generation when the global "create thumbnails" flag is false. + // Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText + if (!picSettings.CreaMiniature) + return; - PrepareSignatureText(imgState); + PrepareSignatureText(imgState); - if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione)) - UpdateFilenameWithCode(imgState); + if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione)) + UpdateFilenameWithCode(imgState); - if (ShouldRenderText()) - CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format); - else - CreateSimpleThumbnail(sourceImage, imgState, format); - } + if (ShouldRenderText()) + CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format); + else + CreateSimpleThumbnail(sourceImage, imgState, format); + } private void PrepareSignatureText(ImageState imgState) { @@ -294,7 +294,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l // This leaves room for margins and prevents clipping int tempFontSize = imgState.DimensioneStandardMiniatura; float maxTextHeight = image.Height * 0.15f; - + while ((textSize.Width > image.Width * 0.95f || textSize.Height > maxTextHeight) && tempFontSize > 5) { tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1; @@ -375,20 +375,20 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l switch (picSettings.Posizione.ToUpper()) { case "ALTO": - { - imgState.YPosFromBottom = picSettings.Margine; - imgState.YPosFromBottom3 = picSettings.MargVert; - break; - } + { + imgState.YPosFromBottom = picSettings.Margine; + imgState.YPosFromBottom3 = picSettings.MargVert; + break; + } case "BASSO": - { - imgState.YPosFromBottom = - Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0))); - imgState.YPosFromBottom3 = - Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0))); - break; - } + { + imgState.YPosFromBottom = + Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0))); + imgState.YPosFromBottom3 = + Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0))); + break; + } } float xCenterOfImg = 0; @@ -396,27 +396,27 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l switch (picSettings.Allineamento.ToUpper()) { case "SINISTRA": - { - xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2))); - if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } + { + xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2))); + if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) + xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); + break; + } case "CENTRO": - { - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } + { + xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); + break; + } case "DESTRA": - { - xCenterOfImg = - Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2))); - if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } + { + xCenterOfImg = + Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2))); + if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) + xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); + break; + } } strFormat.Alignment = StringAlignment.Center; @@ -528,7 +528,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l { logoTransparencyValue = 100; } - + var colorMatrixElements = new[] { new[] { 1.0F, 0.0F, 0.0F, 0.0F, 0.0F }, new[] { 0.0F, 1.0F, 0.0F, 0.0F, 0.0F }, @@ -571,44 +571,44 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l { case "SINISTRA": case "NESSUNA": - { - xPosOfWm = margineUsato; - break; - } + { + xPosOfWm = margineUsato; + break; + } case "CENTRO": - { - xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2); - break; - } + { + xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2); + break; + } case "DESTRA": - { - xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato); - break; - } + { + xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato); + break; + } } switch (logoV) { case "ALTO": case "NESSUNA": - { - yPosOfWm = margineUsato; - break; - } + { + yPosOfWm = margineUsato; + break; + } case "CENTRO": - { - yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2); - break; - } + { + yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2); + break; + } case "BASSO": - { - yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato); - break; - } + { + yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato); + break; + } } grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0, @@ -776,7 +776,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l { // Use 1% of image height as minimum margin, or 10px, whichever is larger float minMargin = Math.Max(10f, imgHeight * 0.01f); - + switch (picSettings.Posizione.ToUpper()) { case "ALTO": @@ -787,18 +787,18 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l case "BASSO": var bottomMargin1 = (float)(imgHeight * picSettings.Margine / 100.0); var bottomMargin4 = (float)(imgHeight * picSettings.MargVert / 100.0); - + // Position from bottom: bottom edge of text at desired margin from bottom // Y = imageHeight - textHeight - bottomMargin var desiredY1 = imgHeight - textHeight - bottomMargin1; var desiredY4 = imgHeight - textHeight - bottomMargin4; - + // Ensure text stays completely within bounds: // - Top edge must be >= minMargin (not clipped at top) // - Bottom edge must be <= imgHeight - minMargin (not clipped at bottom) var maxAllowedY1 = imgHeight - textHeight - minMargin; // Maximum Y to keep bottom margin var maxAllowedY4 = imgHeight - textHeight - minMargin; - + imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(desiredY1, maxAllowedY1)); imgState.YPosFromBottom4 = Math.Max(minMargin, Math.Min(desiredY4, maxAllowedY4)); break; diff --git a/MaddoShared/ImageCreatorAlternate.cs b/MaddoShared/ImageCreatorImageSharp.cs similarity index 100% rename from MaddoShared/ImageCreatorAlternate.cs rename to MaddoShared/ImageCreatorImageSharp.cs diff --git a/docs/image-generation-tests-plan.md b/docs/image-generation-tests-plan.md new file mode 100644 index 0000000..dcddcd8 --- /dev/null +++ b/docs/image-generation-tests-plan.md @@ -0,0 +1,85 @@ +# Image generation test plan — ImageSharp-only (multiplatform) + +Goal +----- +Create an automated, cross-platform test project that validates `ImageCreatorImageSharp` behavior by generating synthetic input images and pixel-inspecting outputs produced by the library. + +Decisions +--------- +- Test only `ImageCreatorImageSharp` (multiplatform). +- Test project targets `net10.0` (not Windows-only) and uses SixLabors.ImageSharp for both generation and verification; avoid `System.Drawing.Common` in tests. +- Inputs are programmatically generated images (no checked-in large binaries). +- Verification uses pixel inspection (sample regions, color averages, non-background pixel counts). + +Project +------- +- Name: `MaddoShared.ImageSharpTests` (folder: `MaddoShared.ImageSharpTests/`) +- TargetFramework: `net10.0` (no Windows-only flags) +- Package references: + - `MSTest.TestFramework` / `MSTest.TestAdapter` / `Microsoft.NET.Test.Sdk` + - `FluentAssertions` + - `Moq` (if needed for loggers) + - `SixLabors.ImageSharp` and `SixLabors.ImageSharp.Drawing` (for image creation and pixel inspection) + - `Microsoft.Extensions.Logging.Abstractions` (lightweight logging) +- ProjectReference: `../MaddoShared/MaddoShared.csproj` + +Helpers (tests) +---------------- +- `TempWorkspace` — creates temporary `Source` and `Dest` folders, cleans up on dispose. +- `TestImageFactory` — creates synthetic JPEG/PNG inputs and in-memory logo PNG bytes. Also can write EXIF orientation values. +- `PixelInspector` — loads output using ImageSharp and exposes: + - `CountNonBackgroundPixels(path, Rectangle region, Rgba32 background, int tolerance)` + - `SampleAverageColor(path, Rectangle region)` + - Region helpers: top/bottom/left/right/center/quadrant rectangles for given image sizes +- `CreatorFactory` — builds `PicSettings` defaults and `ImageCreatorImageSharp` instances. + +Test cases (high-level) +----------------------- +1. Image resizing: verify big and small output dimensions respect settings. +2. Text positioning: for `Posizione` = ALTO/CENTRO/BASSO and `Allineamento` = SINISTRA/CENTRO/DESTRA assert text pixels appear in expected regions. +3. Text content: baseline (empty) vs non-empty comparisons to ensure text changes output; EXIF-vertical photo selects `TestoFirmaV`. +4. Logo positioning: use a solid-color logo (e.g., pure red PNG) and verify red pixels appear in the expected quadrant for combinations of `LogoPosizioneH` × `LogoPosizioneV` and with margins (absolute and percentage). +5. Logo features: opacity (logo color blending) and color-key transparency. +6. EXIF orientation: ensure rotation is applied and output contains no EXIF orientation tag. + +Verification approach +--------------------- +- For text: compare non-background pixel count in the target band vs opposite band (thresholded) to avoid brittle exact glyph placement checks. +- For logo: sample the quadrant where the logo should be; count logo-colored pixels and assert above threshold. +- For opacity: sample average color in logo region and assert it is blended when opacity <100%. + +Scope boundaries +---------------- +- In scope: `ImageCreatorImageSharp` behavior: resize, EXIF rotation, text presence/position, logo position/opactiy, thumbnails. +- Out of scope: `ImageCreatorGDI` (excluded), OCR verification of exact text glyphs, font-subpixel metrics, performance testing. + +Implementation notes +-------------------- +- Keep tests deterministic by creating solid-color inputs and simple logos. +- Use modest image sizes (e.g., 800×600) so tests run fast. +- Carefully choose thresholds for pixel-count assertions to be robust across fonts and rendering differences. + +Run & validate +--------------- +- Build: `dotnet build MaddoShared.ImageSharpTests` +- Test: `dotnet test MaddoShared.ImageSharpTests` + +Files to add +------------ +- `MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj` +- `MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs` +- `MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs` +- `MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs` +- `MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs` +- `MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs` +- `MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs` +- `MaddoShared.ImageSharpTests/Tests/LogoPositioningTests.cs` +- `MaddoShared.ImageSharpTests/Tests/ExifOrientationTests.cs` +- `docs/image-generation-tests-plan.md` (this file) + +Next steps +---------- +1. Create the `MaddoShared.ImageSharpTests` project and add the helper files. +2. Implement the first set of tests (resizing + a simple text presence test) to validate the testing harness. +3. Iterate thresholds and add remaining tests. + diff --git a/imagecatalog/AvaloniaApp.axaml b/imagecatalog/AvaloniaApp.axaml index 8ce9be1..97bd721 100644 --- a/imagecatalog/AvaloniaApp.axaml +++ b/imagecatalog/AvaloniaApp.axaml @@ -1,8 +1,106 @@ + + + + + #F3F5F8 + #FFFFFF + #F7F8FA + #EEF2F6 + #D2D8E0 + + + #1B2027 + #242A33 + #2D343F + #38414E + #4B5563 + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml index 4517b63..ba21b00 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml +++ b/imagecatalog/AvaloniaMainWindow.axaml @@ -1,92 +1,165 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews" + xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia" + x:Class="ImageCatalog_2.AvaloniaMainWindow" + mc:Ignorable="d" + Title="Image Catalog - Avalonia" Height="540" Width="800"> - + - - + + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - - - - - - - + + + - + - - + + - - - + + + - - + + + - - + + + - + diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs index 96dcfc9..2de8d3f 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml.cs +++ b/imagecatalog/AvaloniaMainWindow.axaml.cs @@ -155,12 +155,6 @@ public partial class AvaloniaMainWindow : Window private void UpdateThemeToggleButtonContent() { - var toggleButton = this.FindControl("ThemeToggleButton"); - if (toggleButton is null) - { - return; - } - - toggleButton.Content = _isDarkTheme ? "☀" : "🌙"; + _ = this.FindControl("ThemeToggleButton"); } } diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml b/imagecatalog/AvaloniaViews/AiTabView.axaml index e9652d4..09abb0b 100644 --- a/imagecatalog/AvaloniaViews/AiTabView.axaml +++ b/imagecatalog/AvaloniaViews/AiTabView.axaml @@ -1,40 +1,75 @@ - - - - + + + + + - - - - - + + - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml index e2e22db..7f7f3ae 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -1,34 +1,66 @@ - + - - - + + - + - - - - - - + + + + + + + + + + - + - + - - - - - - - + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + - - - - - - - + + + + + + + + - - - - - + + + + + + + + + + + + + diff --git a/imagecatalog/AvaloniaViews/LogoTabView.axaml b/imagecatalog/AvaloniaViews/LogoTabView.axaml index 30d09d6..275f059 100644 --- a/imagecatalog/AvaloniaViews/LogoTabView.axaml +++ b/imagecatalog/AvaloniaViews/LogoTabView.axaml @@ -2,37 +2,37 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ImageCatalog_2.AvaloniaViews.LogoTabView"> - + - +