diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..2ccc5b4
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,11 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(dotnet build:*)",
+ "Bash(grep -E \"\\\\.\\(cs|xaml\\)$\")",
+ "Bash(gh issue view:*)",
+ "WebSearch",
+ "WebFetch(domain:raw.githubusercontent.com)"
+ ]
+ }
+}
diff --git a/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest b/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest
index c3b035f..b9ccb65 100644
--- a/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest
+++ b/Simple Icon File Maker/Simple Icon File Maker (Package)/Package.appxmanifest
@@ -1,4 +1,4 @@
-
+
+ Version="1.16.0.0" />
diff --git a/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj b/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj
index cf76f85..62884eb 100644
--- a/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj
+++ b/Simple Icon File Maker/Simple Icon File Maker (Package)/Simple Icon File Maker (Package).wapproj
@@ -123,10 +123,10 @@
-
+
build
-
+
build
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Constants/FileTypes.cs b/Simple Icon File Maker/Simple Icon File Maker/Constants/FileTypes.cs
index 7f61d03..5b1d1de 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Constants/FileTypes.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Constants/FileTypes.cs
@@ -4,11 +4,17 @@ namespace Simple_Icon_File_Maker.Constants;
public static class FileTypes
{
- public static readonly HashSet SupportedImageFormats = [".png", ".bmp", ".jpeg", ".jpg", ".ico"];
+ public static readonly HashSet SupportedImageFormats = [".png", ".bmp", ".jpeg", ".jpg", ".ico", ".svg"];
+
+ public static readonly HashSet SupportedDllFormats = [".dll", ".exe", ".mun"];
public static bool IsSupportedImageFormat(this StorageFile file)
{
return SupportedImageFormats.Contains(file.FileType, StringComparer.OrdinalIgnoreCase);
}
+ public static bool IsSupportedDllFormat(this StorageFile file)
+ {
+ return SupportedDllFormats.Contains(file.FileType, StringComparer.OrdinalIgnoreCase);
+ }
}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewImage.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewImage.xaml.cs
index e6ad1d1..dd4bd05 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewImage.xaml.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewImage.xaml.cs
@@ -5,7 +5,9 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
using System.Diagnostics;
+using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Storage;
@@ -21,18 +23,43 @@ public sealed partial class PreviewImage : UserControl
private readonly StorageFile _imageFile;
private readonly int _sideLength = 0;
- public PreviewImage(StorageFile imageFile, int sideLength, string originalName)
+ public PreviewImage(StorageFile imageFile, int sideLength, string originalName, bool showCheckerBackground)
{
InitializeComponent();
_imageFile = imageFile;
_sideLength = sideLength;
+ ShowCheckerBackground = showCheckerBackground;
ToolTipService.SetToolTip(this, $"{sideLength} x {sideLength}");
OriginalName = originalName;
+ ActualThemeChanged += (_, _) =>
+ {
+ int size = isZooming ? ZoomedWidthSpace : _sideLength;
+ mainImageCanvas.Background = _showCheckerBackground
+ ? CreateCheckerBrush(size, _sideLength, ActualTheme)
+ : new SolidColorBrush(Colors.Transparent);
+ };
}
private bool isZooming = false;
+ private bool _showCheckerBackground = true;
public int ZoomedWidthSpace = 100;
+ public bool ShowCheckerBackground
+ {
+ get => _showCheckerBackground;
+ set
+ {
+ if (value != _showCheckerBackground)
+ {
+ _showCheckerBackground = value;
+ int size = isZooming ? ZoomedWidthSpace : _sideLength;
+ mainImageCanvas.Background = _showCheckerBackground
+ ? CreateCheckerBrush(size, _sideLength, ActualTheme)
+ : new SolidColorBrush(Colors.Transparent);
+ }
+ }
+ }
+
public bool ZoomPreview
{
get => isZooming;
@@ -41,9 +68,8 @@ public bool ZoomPreview
if (value != isZooming)
{
isZooming = value;
- mainImageCanvas.Children.Clear();
+ LoadImageOnToCanvas();
InvalidateMeasure();
- InvalidateArrange();
}
}
}
@@ -52,17 +78,13 @@ protected override Size ArrangeOverride(Size finalSize)
{
double dim = Math.Min(finalSize.Width, finalSize.Height);
mainImageCanvas.Arrange(new Rect(new Point((finalSize.Width - dim) / 2, (finalSize.Height - dim) / 2), new Size(dim, dim)));
- LoadImageOnToCanvas();
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
- double dim = Math.Min(availableSize.Width, availableSize.Height);
- // smallerAvailableSize = (int)dim;
- if (double.IsPositiveInfinity(dim))
- dim = 3000;
- return new Size(dim, dim);
+ int size = isZooming ? ZoomedWidthSpace : _sideLength;
+ return new Size(size, size);
}
private async void MenuFlyoutItem_Click(object sender, RoutedEventArgs e)
@@ -106,6 +128,12 @@ private void UserControl_Loaded(object sender, RoutedEventArgs e)
private void LoadImageOnToCanvas()
{
mainImageCanvas.Children.Clear();
+
+ int size = isZooming ? ZoomedWidthSpace : _sideLength;
+ mainImageCanvas.Background = ShowCheckerBackground
+ ? CreateCheckerBrush(size, _sideLength, ActualTheme)
+ : new SolidColorBrush(Colors.Transparent);
+
// from StackOverflow
// user:
// https://stackoverflow.com/users/403671/simon-mourier
@@ -128,10 +156,6 @@ private void LoadImageOnToCanvas()
LoadedImageSurface image = LoadedImageSurface.StartLoadFromUri(new Uri(_imageFile.Path));
brush.Surface = image;
- int size = isZooming ? ZoomedWidthSpace : _sideLength;
- Width = size;
- Height = size;
-
// set the visual size when the image has loaded
image.LoadCompleted += (s, e) =>
{
@@ -153,6 +177,42 @@ private void LoadImageOnToCanvas()
mainImageCanvas.Children.Add(tempGrid);
}
+ private static ImageBrush CreateCheckerBrush(int size, int baseSideLength, ElementTheme theme)
+ {
+ // idealTileSize targets ~8 px at the baseSideLength and scales proportionally for
+ // zoomed views. NearestDivisor then rounds it to the closest exact divisor of
+ // 'size', guaranteeing size % tileSize == 0 — no partial tile, no sliver at the
+ // edge regardless of the canvas size or DPI scale.
+ int idealTileSize = Math.Max(1, (int)Math.Round(8.0 * size / baseSideLength));
+
+ WriteableBitmap bitmap = new(size, size);
+
+ // Light mode: #F0F0F0 / #C4C4C4 — Dark mode: #404040 / #2A2A2A
+ bool isDark = theme == ElementTheme.Dark;
+ byte tileLight = isDark ? (byte)0x40 : (byte)0xF0;
+ byte tileDark = isDark ? (byte)0x2A : (byte)0xC4;
+
+ byte[] pixels = new byte[size * size * 4]; // BGRA format
+ for (int row = 0; row < size; row++)
+ {
+ for (int col = 0; col < size; col++)
+ {
+ bool isLightTile = ((row / idealTileSize) + (col / idealTileSize)) % 2 == 0;
+ byte val = isLightTile ? tileLight : tileDark;
+ int idx = (row * size + col) * 4;
+ pixels[idx] = val; // B
+ pixels[idx + 1] = val; // G
+ pixels[idx + 2] = val; // R
+ pixels[idx + 3] = 255; // A
+ }
+ }
+
+ using Stream stream = bitmap.PixelBuffer.AsStream();
+ stream.Write(pixels, 0, pixels.Length);
+
+ return new ImageBrush { ImageSource = bitmap, Stretch = Stretch.Fill };
+ }
+
private async void ImagePreview_DragStarting(UIElement sender, DragStartingEventArgs args)
{
DragOperationDeferral deferral = args.GetDeferral();
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewStack.xaml b/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewStack.xaml
index a01f15e..8765dc9 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewStack.xaml
+++ b/Simple Icon File Maker/Simple Icon File Maker/Controls/PreviewStack.xaml
@@ -13,11 +13,42 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ChosenSizes { get; private set; }
public bool IsZoomingPreview { get; set; } = false;
+ public bool ShowCheckerBackground { get; set; } = true;
public bool CanRefresh => CheckIfRefreshIsNeeded();
@@ -30,16 +35,22 @@ public sealed partial class PreviewStack : UserControl
public PreviewStack(string path, List sizes, bool showTitle = false)
{
StorageFolder sf = ApplicationData.Current.LocalCacheFolder;
- iconRootString = sf.Path;
+ iconRootString = Path.Combine(sf.Path, Guid.NewGuid().ToString("N"));
ChosenSizes = [.. sizes];
imagePath = path;
- mainImage = new(path);
+ bool isSvgSource = Path.GetExtension(path).Equals(".svg", StringComparison.OrdinalIgnoreCase);
+ mainImage = isSvgSource
+ ? new(path, new MagickReadSettings { BackgroundColor = MagickColors.Transparent })
+ : new(path);
InitializeComponent();
if (showTitle)
+ {
FileNameText.Text = Path.GetFileName(imagePath);
+ SaveColumnButton.Visibility = Visibility.Visible;
+ }
}
public async Task InitializeAsync(IProgress progress)
@@ -156,10 +167,6 @@ public async Task GeneratePreviewImagesAsync(IProgress progress, stri
ImagesProgressBar.Value = 0;
progress.Report(0);
- if (ChosenSizes.Count == 1)
- LoadingText.Text = $"Generating {ChosenSizes.Count} preview for {name}...";
- else
- LoadingText.Text = $"Generating {ChosenSizes.Count} previews for {name}...";
TextAndProgressBar.Visibility = Visibility.Visible;
@@ -173,21 +180,6 @@ public async Task GeneratePreviewImagesAsync(IProgress progress, stri
progress.Report(10);
ImagesProgressBar.Value = 10;
- SourceImageSize ??= new Size((int)mainImage.Width, (int)mainImage.Height);
-
- SmallerSourceSide = Math.Min((int)mainImage.Width, (int)mainImage.Height);
-
- int smallerSide = Math.Min(SourceImageSize.Value.Width, SourceImageSize.Value.Height);
-
- imagePaths.Clear();
- PreviewStackPanel.Children.Clear();
-
- foreach (IconSize iconSize in ChosenSizes)
- {
- iconSize.IsEnabled = true;
- if (iconSize.SideLength > smallerSide)
- iconSize.IsEnabled = false;
- }
if (string.IsNullOrWhiteSpace(imagePath) == true)
{
@@ -195,28 +187,79 @@ public async Task GeneratePreviewImagesAsync(IProgress progress, stri
return false;
}
- try
+ bool isSvg = Path.GetExtension(imagePath).Equals(".svg", StringComparison.OrdinalIgnoreCase);
+ int smallerSide = 0;
+
+ if (isSvg)
{
- _ = await imgFactory.CreateAsync(imagePath);
+ // SVG is vector — it can render at any size, so enable all sizes
+ SmallerSourceSide = int.MaxValue;
+ smallerSide = int.MaxValue;
+ SourceImageSize = null;
+
+ imagePaths.Clear();
+ PreviewStackPanel.Children.Clear();
+
+ foreach (IconSize iconSize in ChosenSizes)
+ iconSize.IsEnabled = true;
+
+ int enabledCount = ChosenSizes.Count(x => x.IsEnabled);
+ if (enabledCount == 1)
+ LoadingText.Text = $"Generating {enabledCount} size for {name}...";
+ else
+ LoadingText.Text = $"Generating {enabledCount} sizes for {name}...";
+
+ progress.Report(15);
+ ImagesProgressBar.Value = 15;
}
- catch (Exception)
+ else
{
- ClearOutputImages();
- return false;
- }
+ IMagickImage? firstPassImage;
+ try
+ {
+ firstPassImage = await imgFactory.CreateAsync(imagePath);
+ }
+ catch (Exception)
+ {
+ ClearOutputImages();
+ return false;
+ }
- progress.Report(15);
- ImagesProgressBar.Value = 15;
- using IMagickImage firstPassImage = await imgFactory.CreateAsync(imagePath);
- IMagickGeometry size = geoFactory.Create(
- (uint)Math.Max(SourceImageSize.Value.Width, SourceImageSize.Value.Height));
- size.IgnoreAspectRatio = false;
- size.FillArea = true;
+ using (firstPassImage)
+ {
+ SourceImageSize = new Size((int)firstPassImage.Width, (int)firstPassImage.Height);
+ SmallerSourceSide = Math.Min((int)firstPassImage.Width, (int)firstPassImage.Height);
+ smallerSide = SmallerSourceSide;
- MagickColor transparent = new("#00000000");
- firstPassImage.Extent(size, Gravity.Center, transparent);
+ imagePaths.Clear();
+ PreviewStackPanel.Children.Clear();
- await firstPassImage.WriteAsync(croppedImagePath, MagickFormat.Png32);
+ foreach (IconSize iconSize in ChosenSizes)
+ {
+ iconSize.IsEnabled = true;
+ if (iconSize.SideLength > smallerSide)
+ iconSize.IsEnabled = false;
+ }
+
+ int enabledCount = ChosenSizes.Count(x => x.IsEnabled);
+ if (enabledCount == 1)
+ LoadingText.Text = $"Generating {enabledCount} size for {name}...";
+ else
+ LoadingText.Text = $"Generating {enabledCount} sizes for {name}...";
+
+ progress.Report(15);
+ ImagesProgressBar.Value = 15;
+ IMagickGeometry size = geoFactory.Create(
+ (uint)Math.Max(SourceImageSize.Value.Width, SourceImageSize.Value.Height));
+ size.IgnoreAspectRatio = false;
+ size.FillArea = true;
+
+ MagickColor transparent = new("#00000000");
+ firstPassImage.Extent(size, Gravity.Center, transparent);
+
+ await firstPassImage.WriteAsync(croppedImagePath, MagickFormat.Png32);
+ }
+ }
MagickImageCollection collection = [];
@@ -234,38 +277,76 @@ public async Task GeneratePreviewImagesAsync(IProgress progress, stri
foreach (int sideLength in selectedSizes)
{
- using IMagickImage image = await imgFactory.CreateAsync(croppedImagePath);
- if (smallerSide < sideLength)
- continue;
-
- currentLocation++;
- progress.Report(baseAtThisPoint + (currentLocation * halfChunkPerImage));
- ImagesProgressBar.Value = baseAtThisPoint + (currentLocation * halfChunkPerImage);
- IMagickGeometry iconSize = geoFactory.Create((uint)sideLength, (uint)sideLength);
- iconSize.IgnoreAspectRatio = false;
-
- if (smallerSide > sideLength)
+ if (isSvg)
{
- await Task.Run(() =>
+ // Render the SVG fresh at each target size for lossless quality
+ MagickReadSettings svgSettings = new()
{
- image.Scale(iconSize);
- image.Sharpen();
- });
+ BackgroundColor = MagickColors.Transparent,
+ Width = (uint)sideLength,
+ Height = (uint)sideLength
+ };
+ using IMagickImage image = await imgFactory.CreateAsync(imagePath, svgSettings);
+
+ // Ensure exact square dimensions with transparent fill
+ IMagickGeometry squareGeo = geoFactory.Create((uint)sideLength);
+ MagickColor transparent = new("#00000000");
+ image.Extent(squareGeo, Gravity.Center, transparent);
+
+ // Final scale to exact size in case the SVG rendered at different dimensions
+ IMagickGeometry exactSize = geoFactory.Create((uint)sideLength, (uint)sideLength);
+ exactSize.IgnoreAspectRatio = true;
+ await Task.Run(() => image.Scale(exactSize));
+
+ currentLocation++;
+ progress.Report(baseAtThisPoint + (currentLocation * halfChunkPerImage));
+ ImagesProgressBar.Value = baseAtThisPoint + (currentLocation * halfChunkPerImage);
+
+ string iconPathSvg = $"{iconRootString}\\{Random.Shared.Next()}Image{sideLength}.png";
+ if (File.Exists(iconPathSvg))
+ File.Delete(iconPathSvg);
+
+ await image.WriteAsync(iconPathSvg, MagickFormat.Png32);
+ collection.Add(iconPathSvg);
+ imagePaths.Add((sideLength.ToString(), iconPathSvg));
+
+ currentLocation++;
+ progress.Report(baseAtThisPoint + (currentLocation * halfChunkPerImage));
+ ImagesProgressBar.Value = baseAtThisPoint + (currentLocation * halfChunkPerImage);
}
+ else
+ {
+ using IMagickImage image = await imgFactory.CreateAsync(croppedImagePath);
+ if (smallerSide < sideLength)
+ continue;
- string iconPath = $"{iconRootString}\\{Random.Shared.Next()}Image{sideLength}.png";
-
- if (File.Exists(iconPath))
- File.Delete(iconPath);
-
- await image.WriteAsync(iconPath, MagickFormat.Png32);
-
- collection.Add(iconPath);
- imagePaths.Add((sideLength.ToString(), iconPath));
+ currentLocation++;
+ progress.Report(baseAtThisPoint + (currentLocation * halfChunkPerImage));
+ ImagesProgressBar.Value = baseAtThisPoint + (currentLocation * halfChunkPerImage);
+ IMagickGeometry iconSize = geoFactory.Create((uint)sideLength, (uint)sideLength);
+ iconSize.IgnoreAspectRatio = false;
- currentLocation++;
- progress.Report(baseAtThisPoint + (currentLocation * halfChunkPerImage));
- ImagesProgressBar.Value = baseAtThisPoint + (currentLocation * halfChunkPerImage);
+ if (smallerSide > sideLength)
+ {
+ await Task.Run(() =>
+ {
+ image.Scale(iconSize);
+ image.Sharpen();
+ });
+ }
+
+ string iconPath = $"{iconRootString}\\{Random.Shared.Next()}Image{sideLength}.png";
+ if (File.Exists(iconPath))
+ File.Delete(iconPath);
+
+ await image.WriteAsync(iconPath, MagickFormat.Png32);
+ collection.Add(iconPath);
+ imagePaths.Add((sideLength.ToString(), iconPath));
+
+ currentLocation++;
+ progress.Report(baseAtThisPoint + (currentLocation * halfChunkPerImage));
+ ImagesProgressBar.Value = baseAtThisPoint + (currentLocation * halfChunkPerImage);
+ }
}
try
@@ -297,6 +378,8 @@ private async Task OpenIconFile(IProgress progress)
imagePaths.Clear();
PreviewStackPanel.Children.Clear();
+ Directory.CreateDirectory(iconRootString);
+
MagickImageCollection collection = new(imagePath);
List<(string, string)> iconImages = [];
@@ -327,7 +410,7 @@ private async Task OpenIconFile(IProgress progress)
int sideLength = (int)image.Width;
StorageFile imageSF = await StorageFile.GetFileFromPathAsync(pathForSingleImage);
- PreviewImage previewImage = new(imageSF, sideLength, imageName);
+ PreviewImage previewImage = new(imageSF, sideLength, imageName, ShowCheckerBackground);
PreviewStackPanel.Children.Add(previewImage);
currentLocation++;
@@ -368,7 +451,7 @@ public async Task UpdatePreviewsAsync()
StorageFile imageSF = await StorageFile.GetFileFromPathAsync(imagePath);
- PreviewImage image = new(imageSF, sideLength, originalName);
+ PreviewImage image = new(imageSF, sideLength, originalName, ShowCheckerBackground);
PreviewStackPanel.Children.Add(image);
}
@@ -414,6 +497,41 @@ public void UpdateSizeAndZoom()
if (!double.IsNaN(ActualWidth) && ActualWidth > 40)
img.ZoomedWidthSpace = (int)ActualWidth - 40;
img.ZoomPreview = IsZoomingPreview;
+ img.ShowCheckerBackground = ShowCheckerBackground;
}
}
+
+ private async void SaveIconMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ await SaveColumnWithPickerAsync(saveAllImages: false);
+ }
+
+ private async void SaveAllImagesMenuItem_Click(object sender, RoutedEventArgs e)
+ {
+ await SaveColumnWithPickerAsync(saveAllImages: true);
+ }
+
+ private async Task SaveColumnWithPickerAsync(bool saveAllImages)
+ {
+ FileSavePicker savePicker = new()
+ {
+ SuggestedStartLocation = PickerLocationId.PicturesLibrary,
+ SuggestedFileName = Path.GetFileNameWithoutExtension(imagePath),
+ DefaultFileExtension = ".ico",
+ };
+ savePicker.FileTypeChoices.Add("ICO File", [".ico"]);
+ InitializeWithWindow.Initialize(savePicker, App.MainWindow.GetWindowHandle());
+
+ StorageFile? file = await savePicker.PickSaveFileAsync();
+ if (file is null)
+ return;
+
+ IIconSizesService iconSizesService = App.GetService();
+ IconSortOrder sortOrder = iconSizesService.SortOrder;
+
+ if (saveAllImages)
+ await SaveAllImagesAsync(file.Path, sortOrder);
+ else
+ await SaveIconAsync(file.Path, sortOrder);
+ }
}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Helpers/DllIconExtractorHelper.cs b/Simple Icon File Maker/Simple Icon File Maker/Helpers/DllIconExtractorHelper.cs
new file mode 100644
index 0000000..e3d578e
--- /dev/null
+++ b/Simple Icon File Maker/Simple Icon File Maker/Helpers/DllIconExtractorHelper.cs
@@ -0,0 +1,210 @@
+using ImageMagick;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Simple_Icon_File_Maker.Helpers;
+
+[SupportedOSPlatform("windows")]
+public static class DllIconExtractorHelper
+{
+ private const int RT_ICON = 3;
+ private const int RT_GROUP_ICON = 14;
+ private const uint LOAD_LIBRARY_AS_DATAFILE = 0x00000002;
+ private static readonly byte[] PngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern nint LoadLibraryEx(string lpFileName, nint hFile, uint dwFlags);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool FreeLibrary(nint hModule);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
+ private static extern nint FindResource(nint hModule, nint lpName, nint lpType);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern nint LoadResource(nint hModule, nint hResInfo);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern nint LockResource(nint hResData);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern uint SizeofResource(nint hModule, nint hResInfo);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool EnumResourceNames(nint hModule, nint lpszType, EnumResNameProc lpEnumFunc, nint lParam);
+
+ private delegate bool EnumResNameProc(nint hModule, nint lpszType, nint lpszName, nint lParam);
+
+ public static Task> ExtractIconsToFolderAsync(string dllPath, string outputFolder)
+ {
+ return Task.Run>(() =>
+ {
+ List savedPaths = [];
+ Directory.CreateDirectory(outputFolder);
+
+ nint hModule = LoadLibraryEx(dllPath, 0, LOAD_LIBRARY_AS_DATAFILE);
+ if (hModule == 0)
+ return savedPaths;
+
+ try
+ {
+ ExtractGroupIcons(hModule, outputFolder, savedPaths);
+ }
+ finally
+ {
+ FreeLibrary(hModule);
+ }
+
+ return savedPaths;
+ });
+ }
+
+ private static void ExtractGroupIcons(nint hModule, string outputFolder, List savedPaths)
+ {
+ EnumResourceNames(hModule, RT_GROUP_ICON, (hMod, _, lpszName, _) =>
+ {
+ nint hResInfo = FindResource(hMod, lpszName, RT_GROUP_ICON);
+ if (hResInfo == 0)
+ return true;
+
+ byte[]? groupData = ReadResourceBytes(hMod, hResInfo);
+ if (groupData is null || groupData.Length < 6)
+ return true;
+
+ // GRPICONDIR: reserved(2) + type(2) + count(2)
+ int count = BitConverter.ToInt16(groupData, 4);
+ string groupName = GetResourceName(lpszName);
+
+ // Pick the entry with the largest pixel dimensions — that is the best source
+ // image for the user to scale down from. All entries in the same group are
+ // different-resolution versions of the same icon; we only need one source.
+ int bestDim = -1;
+ ushort bestNId = 0;
+ int bestDisplayWidth = 0;
+ int bestDisplayHeight = 0;
+
+ for (int j = 0; j < count; j++)
+ {
+ // GRPICONDIRENTRY starts at offset 6, each entry is 14 bytes
+ int entryOffset = 6 + j * 14;
+ if (entryOffset + 14 > groupData.Length)
+ break;
+
+ byte width = groupData[entryOffset];
+ byte height = groupData[entryOffset + 1];
+ int displayWidth = width == 0 ? 256 : width;
+ int displayHeight = height == 0 ? 256 : height;
+ int dim = displayWidth * displayHeight;
+
+ if (dim > bestDim)
+ {
+ bestDim = dim;
+ // nId is at offset +12 within the entry (2 bytes)
+ bestNId = BitConverter.ToUInt16(groupData, entryOffset + 12);
+ bestDisplayWidth = displayWidth;
+ bestDisplayHeight = displayHeight;
+ }
+ }
+
+ if (bestDim < 0)
+ return true;
+
+ nint hIconRes = FindResource(hMod, bestNId, RT_ICON);
+ if (hIconRes == 0)
+ return true;
+
+ byte[]? iconData = ReadResourceBytes(hMod, hIconRes);
+ if (iconData is null)
+ return true;
+
+ string outputPath = Path.Combine(outputFolder, $"{groupName}.png");
+
+ try
+ {
+ SaveIconAsPng(iconData, bestDisplayWidth, bestDisplayHeight, outputPath);
+ savedPaths.Add(outputPath);
+ }
+ catch
+ {
+ // Skip icons that fail to convert
+ }
+
+ return true;
+ }, 0);
+ }
+
+ private static string GetResourceName(nint lpszName)
+ {
+ // IS_INTRESOURCE: pointer value <= 0xFFFF
+ if ((ulong)lpszName <= 0xFFFF)
+ return lpszName.ToString();
+
+ return Marshal.PtrToStringUni(lpszName) ?? lpszName.ToString();
+ }
+
+ private static byte[]? ReadResourceBytes(nint hModule, nint hResInfo)
+ {
+ uint size = SizeofResource(hModule, hResInfo);
+ if (size == 0)
+ return null;
+
+ nint hResData = LoadResource(hModule, hResInfo);
+ if (hResData == 0)
+ return null;
+
+ nint pData = LockResource(hResData);
+ if (pData == 0)
+ return null;
+
+ byte[] data = new byte[size];
+ Marshal.Copy(pData, data, 0, (int)size);
+ return data;
+ }
+
+ private static void SaveIconAsPng(byte[] iconData, int width, int height, string outputPath)
+ {
+ // Windows Vista+ stores 256x256 RT_ICON entries as raw PNG data
+ if (iconData.Length >= 8 && iconData.AsSpan(0, 8).SequenceEqual(PngSignature))
+ {
+ File.WriteAllBytes(outputPath, iconData);
+ return;
+ }
+
+ // DIB data — wrap in a minimal ICO container so ImageMagick can decode it
+ byte[] icoBytes = BuildIcoFile(iconData, width, height);
+ MagickReadSettings settings = new() { Format = MagickFormat.Ico };
+ using MagickImage image = new(icoBytes, settings);
+ image.Write(outputPath, MagickFormat.Png);
+ }
+
+ private static byte[] BuildIcoFile(byte[] iconData, int width, int height)
+ {
+ // ICO header: 6 bytes
+ // ICONDIRENTRY: 16 bytes
+ // Total header: 22 bytes, image data follows
+ const int headerSize = 22;
+ byte[] ico = new byte[headerSize + iconData.Length];
+ using MemoryStream ms = new(ico);
+ using BinaryWriter w = new(ms);
+
+ // ICONDIR
+ w.Write((ushort)0); // reserved
+ w.Write((ushort)1); // type = 1 (icon)
+ w.Write((ushort)1); // count = 1
+
+ // ICONDIRENTRY
+ w.Write((byte)(width >= 256 ? 0 : width)); // width (0 = 256)
+ w.Write((byte)(height >= 256 ? 0 : height)); // height (0 = 256)
+ w.Write((byte)0); // color count
+ w.Write((byte)0); // reserved
+ w.Write((ushort)1); // planes
+ w.Write((ushort)32); // bit count
+ w.Write((uint)iconData.Length); // size of image data
+ w.Write((uint)headerSize); // offset of image data
+
+ // Image data
+ w.Write(iconData);
+
+ return ico;
+ }
+}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs b/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs
index b39c1ca..d1afb75 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Helpers/FilePickerHelper.cs
@@ -1,32 +1,55 @@
+using Simple_Icon_File_Maker.Constants;
using Windows.Storage;
using Windows.Storage.Pickers;
+using WinRT.Interop;
namespace Simple_Icon_File_Maker.Helpers;
public static class FilePickerHelper
{
- public static async Task TrySetSuggestedFolderFromSourceImage(FileSavePicker savePicker, string imagePath)
+ public static FileOpenPicker CreateDllPicker()
{
- if (string.IsNullOrWhiteSpace(imagePath))
- return;
+ FileOpenPicker picker = new()
+ {
+ ViewMode = PickerViewMode.List,
+ SuggestedStartLocation = PickerLocationId.ComputerFolder,
+ };
+
+ foreach (string ext in FileTypes.SupportedDllFormats)
+ picker.FileTypeFilter.Add(ext);
+
+ InitializeWithWindow.Initialize(picker, App.MainWindow.WindowHandle);
+
+ return picker;
+ }
- try
+ public static async Task CreateSavePicker(string OutputPath, string ImagePath)
+ {
+ FileSavePicker savePicker = new()
{
- // Use the source image file itself to suggest the folder
- // This makes the picker open in the source image's folder
- if (File.Exists(imagePath))
- {
- StorageFile sourceFile = await StorageFile.GetFileFromPathAsync(imagePath);
- savePicker.SuggestedSaveFile = sourceFile;
+ SuggestedStartLocation = PickerLocationId.PicturesLibrary,
- // SuggestedSaveFile overrides SuggestedFileName, so re-set
- // the name without the source extension to avoid names like "file.png.ico"
- savePicker.SuggestedFileName = Path.GetFileNameWithoutExtension(imagePath);
+ DefaultFileExtension = ".ico",
+ FileTypeChoices =
+ {
+ { "ICO File", [".ico"] }
}
- }
- catch
+ };
+
+ if (!string.IsNullOrWhiteSpace(OutputPath) && File.Exists(OutputPath))
{
- // If file access fails, fall back to default picker behavior
+ try
+ {
+ StorageFile previousFile = await StorageFile.GetFileFromPathAsync(OutputPath);
+ savePicker.SuggestedSaveFile = previousFile;
+ }
+ catch { }
}
+
+ savePicker.SuggestedFileName = Path.GetFileNameWithoutExtension(ImagePath) + ".ico";
+
+ InitializeWithWindow.Initialize(savePicker, App.MainWindow.WindowHandle);
+
+ return savePicker;
}
}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Helpers/ImageHelper.cs b/Simple Icon File Maker/Simple Icon File Maker/Helpers/ImageHelper.cs
index b84e001..6170f50 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Helpers/ImageHelper.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Helpers/ImageHelper.cs
@@ -14,8 +14,25 @@ public static class ImageHelper
try
{
MagickImage image;
+ string extension = Path.GetExtension(imagePath);
+
+ // For .svg files, rasterize at 512px with transparent background for preview
+ if (extension.Equals(".svg", StringComparison.InvariantCultureIgnoreCase))
+ {
+ MagickReadSettings svgSettings = new()
+ {
+ BackgroundColor = MagickColors.Transparent,
+ Width = 512,
+ Height = 512
+ };
+ image = new(imagePath, svgSettings);
+ MagickGeometry squareGeo = new(512u);
+ image.Extent(squareGeo, Gravity.Center, new MagickColor("#00000000"));
+ return image;
+ }
+
// For .ico files, load the largest frame instead of the first one
- if (Path.GetExtension(imagePath).Equals(".ico", StringComparison.InvariantCultureIgnoreCase))
+ if (extension.Equals(".ico", StringComparison.InvariantCultureIgnoreCase))
{
MagickImageCollection collection = new(imagePath);
// Find the largest frame by area (width * height)
@@ -62,6 +79,10 @@ public static int GetSmallerImageSide(string imagePath)
{
try
{
+ // SVG is vector and can render at any size — treat as effectively unlimited
+ if (Path.GetExtension(imagePath).Equals(".svg", StringComparison.OrdinalIgnoreCase))
+ return int.MaxValue;
+
MagickImage image = new(imagePath);
return (int)Math.Min(image.Width, image.Height);
}
@@ -76,7 +97,9 @@ public static async Task ApplyGrayscaleAsync(string imagePath, Image? di
StorageFolder sf = ApplicationData.Current.LocalCacheFolder;
string fileName = Path.GetFileNameWithoutExtension(imagePath);
string extension = Path.GetExtension(imagePath);
- string grayFilePath = Path.Combine(sf.Path, $"{fileName}_gray{extension}");
+ // SVG must be rasterized before filters can be applied — save output as PNG
+ string outputExtension = extension.Equals(".svg", StringComparison.OrdinalIgnoreCase) ? ".png" : extension;
+ string grayFilePath = Path.Combine(sf.Path, $"{fileName}_gray{outputExtension}");
MagickImage image = new(imagePath);
image.Grayscale();
@@ -93,7 +116,8 @@ public static async Task ApplyBlackWhiteOtsuAsync(string imagePath, Imag
StorageFolder sf = ApplicationData.Current.LocalCacheFolder;
string fileName = Path.GetFileNameWithoutExtension(imagePath);
string extension = Path.GetExtension(imagePath);
- string bwFilePath = Path.Combine(sf.Path, $"{fileName}_bw{extension}");
+ string outputExtension = extension.Equals(".svg", StringComparison.OrdinalIgnoreCase) ? ".png" : extension;
+ string bwFilePath = Path.Combine(sf.Path, $"{fileName}_bw{outputExtension}");
MagickImage image = new(imagePath);
image.Grayscale();
@@ -111,7 +135,8 @@ public static async Task ApplyBlackWhiteKapurAsync(string imagePath, Ima
StorageFolder sf = ApplicationData.Current.LocalCacheFolder;
string fileName = Path.GetFileNameWithoutExtension(imagePath);
string extension = Path.GetExtension(imagePath);
- string bwkFilePath = Path.Combine(sf.Path, $"{fileName}_bwk{extension}");
+ string outputExtension = extension.Equals(".svg", StringComparison.OrdinalIgnoreCase) ? ".png" : extension;
+ string bwkFilePath = Path.Combine(sf.Path, $"{fileName}_bwk{outputExtension}");
MagickImage image = new(imagePath);
image.Grayscale();
@@ -129,7 +154,8 @@ public static async Task ApplyInvertAsync(string imagePath, Image? displ
StorageFolder sf = ApplicationData.Current.LocalCacheFolder;
string fileName = Path.GetFileNameWithoutExtension(imagePath);
string extension = Path.GetExtension(imagePath);
- string invFilePath = Path.Combine(sf.Path, $"{fileName}_inv{extension}");
+ string outputExtension = extension.Equals(".svg", StringComparison.OrdinalIgnoreCase) ? ".png" : extension;
+ string invFilePath = Path.Combine(sf.Path, $"{fileName}_inv{outputExtension}");
MagickImage image = new(imagePath);
image.Negate(Channels.RGB);
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Models/FileGroupItem.cs b/Simple Icon File Maker/Simple Icon File Maker/Models/FileGroupItem.cs
new file mode 100644
index 0000000..f308741
--- /dev/null
+++ b/Simple Icon File Maker/Simple Icon File Maker/Models/FileGroupItem.cs
@@ -0,0 +1,23 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Simple_Icon_File_Maker.Models;
+
+public partial class FileGroupItem : ObservableObject
+{
+ [ObservableProperty]
+ public partial string Extension { get; set; } = string.Empty;
+
+ [ObservableProperty]
+ public partial int TotalCount { get; set; }
+
+ [ObservableProperty]
+ public partial int LargeFileCount { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsIncluded { get; set; } = true;
+
+ // Read-only label for CheckBox Content — built once, never mutates after creation
+ public string DisplayLabel =>
+ $"{Extension} — {TotalCount} file{(TotalCount == 1 ? "" : "s")}" +
+ (LargeFileCount > 0 ? $" ({LargeFileCount} over 5 MB)" : string.Empty);
+}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Services/ThemeSelectorService.cs b/Simple Icon File Maker/Simple Icon File Maker/Services/ThemeSelectorService.cs
index 919c86d..6df1dd0 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Services/ThemeSelectorService.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Services/ThemeSelectorService.cs
@@ -33,6 +33,9 @@ public async Task SetThemeAsync(ElementTheme theme)
public async Task SetRequestedThemeAsync()
{
+ if (App.MainWindow.Content is FrameworkElement rootElement)
+ rootElement.RequestedTheme = Theme;
+
await Task.CompletedTask;
}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj b/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj
index 76c6aa4..fda98ab 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj
+++ b/Simple Icon File Maker/Simple Icon File Maker/Simple Icon File Maker.csproj
@@ -2,7 +2,7 @@
WinExe
net9.0-windows10.0.26100.0
- 10.0.26100.38
+ 10.0.26100.56
10.0.19041.0
10.0.19041.0
Simple_Icon_File_Maker
@@ -31,6 +31,8 @@
+
+
@@ -39,15 +41,16 @@
-
+
+
-
+
-
-
+
+
@@ -81,6 +84,12 @@
$(DefaultXamlRuntime)
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
$(DefaultXamlRuntime)
diff --git a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/AboutViewModel.cs b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/AboutViewModel.cs
index d77b256..15fe09f 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/AboutViewModel.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/AboutViewModel.cs
@@ -1,11 +1,16 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using Microsoft.UI.Xaml;
using Simple_Icon_File_Maker.Contracts.Services;
namespace Simple_Icon_File_Maker.ViewModels;
public partial class AboutViewModel : ObservableRecipient
{
+ private readonly IThemeSelectorService _themeSelectorService;
+
+ public ElementTheme Theme => _themeSelectorService.Theme;
+
[RelayCommand]
public void GoBack()
{
@@ -17,8 +22,15 @@ private INavigationService NavigationService
get;
}
- public AboutViewModel(INavigationService navigationService)
+ public AboutViewModel(INavigationService navigationService, IThemeSelectorService themeSelectorService)
{
NavigationService = navigationService;
+ _themeSelectorService = themeSelectorService;
+ }
+
+ public async Task SwitchThemeAsync(ElementTheme theme)
+ {
+ await _themeSelectorService.SetThemeAsync(theme);
+ OnPropertyChanged(nameof(Theme));
}
}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs
index 091cc94..d87b068 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MainViewModel.cs
@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
using Simple_Icon_File_Maker.Constants;
using Simple_Icon_File_Maker.Contracts.Services;
using Simple_Icon_File_Maker.Contracts.ViewModels;
@@ -15,6 +16,7 @@
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
+using Windows.Storage.Streams;
using Windows.System;
using WinRT.Interop;
@@ -29,9 +31,10 @@ public partial class MainViewModel : ObservableRecipient, INavigationAware, IDis
private const int SettingsSaveDelayMs = 300; // Delay before saving settings
private const int UiInitializationDelayMs = 200; // Delay to allow UI to initialize
private int _countdownElapsedMs = 0;
- private System.Timers.Timer _settingsSaveTimer = new();
+ private readonly System.Timers.Timer _settingsSaveTimer = new();
private readonly UndoRedo _undoRedo = new();
private CancellationTokenSource? _loadImageCancellationTokenSource;
+ private bool _hasNavigatedOnce;
private bool _disposed;
private readonly ILocalSettingsService _localSettingsService;
@@ -41,6 +44,9 @@ public partial class MainViewModel : ObservableRecipient, INavigationAware, IDis
[ObservableProperty]
public partial bool IsAutoRefreshEnabled { get; set; } = true;
+ [ObservableProperty]
+ public partial bool IsCheckerBackgroundVisible { get; set; } = false;
+
[ObservableProperty]
public partial double CountdownProgress { get; set; } = 0;
@@ -48,6 +54,7 @@ public partial class MainViewModel : ObservableRecipient, INavigationAware, IDis
public partial bool IsCountdownActive { get; set; } = false;
[ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(CropImageCommand))]
public partial string ImagePath { get; set; } = "";
[ObservableProperty]
@@ -133,6 +140,23 @@ partial void OnIsAutoRefreshEnabledChanged(bool value)
_settingsSaveTimer.Start();
}
+ partial void OnIsCheckerBackgroundVisibleChanged(bool value)
+ {
+ if (PreviewsGrid is null)
+ return;
+
+ foreach (UIElement element in PreviewsGrid.Children)
+ {
+ if (element is Controls.PreviewStack stack)
+ {
+ stack.ShowCheckerBackground = value;
+ stack.UpdateSizeAndZoom();
+ }
+ }
+
+ _localSettingsService.SaveSettingAsync(nameof(IsCheckerBackgroundVisible), value);
+ }
+
private async void SettingsSaveTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
_settingsSaveTimer.Stop();
@@ -150,6 +174,15 @@ public async void OnNavigatedTo(object parameter)
IsAutoRefreshEnabled = true;
}
+ try
+ {
+ IsCheckerBackgroundVisible = await _localSettingsService.ReadSettingAsync(nameof(IsCheckerBackgroundVisible));
+ }
+ catch (Exception)
+ {
+ IsCheckerBackgroundVisible = false;
+ }
+
ShowUpgradeToProButton = !_storeService.OwnsPro;
// Load shared image path from share target activation
@@ -158,12 +191,18 @@ public async void OnNavigatedTo(object parameter)
ImagePath = App.SharedImagePath;
App.SharedImagePath = null;
}
- // Load CLI args if present
- else if (App.cliArgs?.Length > 1)
+ // Load CLI args if present on first navigation only
+ else if (!_hasNavigatedOnce && App.cliArgs?.Length > 1)
{
ImagePath = App.cliArgs[1];
}
+ _hasNavigatedOnce = true;
+
+ // Skip reloading if an image is already loaded (returning from About page)
+ if (IsImageSelected)
+ return;
+
LoadIconSizes();
// Delayed load to allow UI to initialize
@@ -177,7 +216,6 @@ public async void OnNavigatedTo(object parameter)
public void OnNavigatedFrom()
{
_loadImageCancellationTokenSource?.Cancel();
- Dispose();
}
[RelayCommand]
@@ -206,7 +244,7 @@ public async Task NavigateToMulti()
StorageFolder folder = await picker.PickSingleFolderAsync();
if (folder is not null)
- NavigationService.NavigateTo(typeof(MultiViewModel).FullName!, folder);
+ NavigationService.NavigateTo(typeof(MultiViewModel).FullName!, new MultiPageParameter(folder));
}
else
{
@@ -242,6 +280,35 @@ public async Task BrowseAndSelectImage()
await LoadFromImagePathAsync(_loadImageCancellationTokenSource.Token);
}
+ [RelayCommand]
+ public async Task BrowseAndSelectDll()
+ {
+ FileOpenPicker picker = FilePickerHelper.CreateDllPicker();
+
+ StorageFile file = await picker.PickSingleFileAsync();
+ if (file is null)
+ return;
+
+ string tempFolderPath = Path.Combine(
+ ApplicationData.Current.LocalCacheFolder.Path,
+ "dll_extract");
+
+ if (Directory.Exists(tempFolderPath))
+ Directory.Delete(tempFolderPath, recursive: true);
+
+ IReadOnlyList extractedPaths =
+ await DllIconExtractorHelper.ExtractIconsToFolderAsync(file.Path, tempFolderPath);
+
+ if (extractedPaths.Count == 0)
+ {
+ ShowError($"No icons found in {file.Name}.");
+ return;
+ }
+
+ StorageFolder extractFolder = await StorageFolder.GetFolderFromPathAsync(tempFolderPath);
+ NavigationService.NavigateTo(typeof(MultiViewModel).FullName!, new MultiPageParameter(extractFolder, IsFromDllExtraction: true, SourceFilePath: file.Path));
+ }
+
[RelayCommand]
public async Task ClearImage()
{
@@ -369,9 +436,7 @@ public async Task SaveIcon()
{
try
{
- FileSavePicker savePicker = CreateSavePicker();
- await FilePickerHelper.TrySetSuggestedFolderFromSourceImage(savePicker, ImagePath);
- InitializeWithWindow.Initialize(savePicker, App.MainWindow.WindowHandle);
+ FileSavePicker savePicker = await FilePickerHelper.CreateSavePicker(OutputPath, ImagePath);
StorageFile file = await savePicker.PickSaveFileAsync();
@@ -410,9 +475,7 @@ public async Task SaveAllImages()
{
try
{
- FileSavePicker savePicker = CreateSavePicker();
- await FilePickerHelper.TrySetSuggestedFolderFromSourceImage(savePicker, ImagePath);
- InitializeWithWindow.Initialize(savePicker, App.MainWindow.WindowHandle);
+ FileSavePicker savePicker = await FilePickerHelper.CreateSavePicker(OutputPath, ImagePath);
StorageFile file = await savePicker.PickSaveFileAsync();
@@ -624,9 +687,8 @@ public async Task RemoveBackground()
if (dialog.ResultImagePath is not null)
{
- ImageMagick.MagickImage resultImage = new(dialog.ResultImagePath);
- if (MainImage != null)
- MainImage.Source = resultImage.ToImageSource();
+ MagickImage resultImage = new(dialog.ResultImagePath);
+ MainImage?.Source = resultImage.ToImageSource();
MagickImageUndoRedoItem undoRedoItem = new(MainImage!, ImagePath, dialog.ResultImagePath);
_undoRedo.AddUndo(undoRedoItem);
@@ -643,6 +705,41 @@ public async Task RemoveBackground()
}
}
+ private bool CanCropImage() =>
+ !string.IsNullOrWhiteSpace(ImagePath) &&
+ !Path.GetExtension(ImagePath).Equals(".svg", StringComparison.OrdinalIgnoreCase);
+
+ [RelayCommand(CanExecute = nameof(CanCropImage))]
+ public async Task CropImage()
+ {
+ if (string.IsNullOrWhiteSpace(ImagePath))
+ return;
+
+ try
+ {
+ CropImageDialog dialog = new(ImagePath);
+ await NavigationService.ShowModal(dialog);
+
+ if (dialog.ResultImagePath is not null)
+ {
+ MagickImage resultImage = new(dialog.ResultImagePath);
+ MainImage?.Source = resultImage.ToImageSource();
+
+ MagickImageUndoRedoItem undoRedoItem = new(MainImage!, ImagePath, dialog.ResultImagePath);
+ _undoRedo.AddUndo(undoRedoItem);
+ UpdateUndoRedoState();
+
+ ImagePath = dialog.ResultImagePath;
+ await RefreshPreviews();
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to crop image: {ex.Message}");
+ ShowError($"Failed to crop image: {ex.Message}");
+ }
+ }
+
[RelayCommand]
public async Task Undo()
{
@@ -788,7 +885,9 @@ public async Task HandleDrop(DragEventArgs e)
}
}
+#pragma warning disable CA1822 // Mark members as static
public void HandleDragOver(DragEventArgs e)
+#pragma warning restore CA1822 // Mark members as static
{
DataPackageView dataView = e.DataView;
@@ -898,19 +997,42 @@ private async Task LoadFromImagePathAsync(CancellationToken cancellationToken =
{
cancellationToken.ThrowIfCancellationRequested();
- MagickImage? image = await ImageHelper.LoadImageAsync(ImagePath);
+ if (Path.GetExtension(ImagePath).Equals(".svg", StringComparison.OrdinalIgnoreCase))
+ {
+ // Use WinUI 3 native SvgImageSource for lossless vector preview
+ StorageFile svgFile = await StorageFile.GetFileFromPathAsync(ImagePath);
+ using IRandomAccessStream stream = await svgFile.OpenReadAsync();
+ SvgImageSource svgSource = new();
+ SvgImageSourceLoadStatus loadStatus = await svgSource.SetSourceAsync(stream);
- cancellationToken.ThrowIfCancellationRequested();
+ cancellationToken.ThrowIfCancellationRequested();
- if (image == null)
- {
- ShowError("Failed to load image");
- IsLoading = false;
- IsImageSelected = false;
- return;
+ if (loadStatus != SvgImageSourceLoadStatus.Success)
+ {
+ ShowError("Failed to load SVG image");
+ IsLoading = false;
+ IsImageSelected = false;
+ return;
+ }
+
+ MainImageSource = svgSource;
}
+ else
+ {
+ MagickImage? image = await ImageHelper.LoadImageAsync(ImagePath);
+
+ cancellationToken.ThrowIfCancellationRequested();
- MainImageSource = image.ToImageSource();
+ if (image == null)
+ {
+ ShowError("Failed to load image");
+ IsLoading = false;
+ IsImageSelected = false;
+ return;
+ }
+
+ MainImageSource = image.ToImageSource();
+ }
}
catch (OperationCanceledException)
{
@@ -937,7 +1059,8 @@ private async Task LoadFromImagePathAsync(CancellationToken cancellationToken =
List selectedSizes = [.. SizesControl.ViewModel.IconSizes.Where(x => x.IsSelected)];
Controls.PreviewStack previewStack = new(ImagePath, selectedSizes)
{
- SortOrder = _iconSizesService.SortOrder
+ SortOrder = _iconSizesService.SortOrder,
+ ShowCheckerBackground = IsCheckerBackgroundVisible
};
PreviewsGrid?.Children.Add(previewStack);
@@ -1031,32 +1154,6 @@ private void SelectIconSizesFromPreview()
SizesControl.UpdateEnabledSizes(smallerSide);
}
- private FileSavePicker CreateSavePicker()
- {
- FileSavePicker savePicker = new()
- {
- SuggestedStartLocation = PickerLocationId.PicturesLibrary,
- };
- savePicker.FileTypeChoices.Add("ICO File", [".ico"]);
- savePicker.DefaultFileExtension = ".ico";
- savePicker.SuggestedFileName = Path.GetFileNameWithoutExtension(ImagePath);
-
- if (!string.IsNullOrWhiteSpace(OutputPath) && File.Exists(OutputPath))
- {
- try
- {
- Task.Run(async () =>
- {
- StorageFile previousFile = await StorageFile.GetFileFromPathAsync(OutputPath);
- savePicker.SuggestedSaveFile = previousFile;
- }).Wait();
- }
- catch { }
- }
-
- return savePicker;
- }
-
private void ShowError(string message)
{
ErrorMessage = message;
diff --git a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MultiViewModel.cs b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MultiViewModel.cs
index 3a96e3e..50c9c5f 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MultiViewModel.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/ViewModels/MultiViewModel.cs
@@ -13,9 +13,14 @@
namespace Simple_Icon_File_Maker.ViewModels;
+public record MultiPageParameter(StorageFolder Folder, bool IsFromDllExtraction = false, string? SourceFilePath = null);
+
public partial class MultiViewModel : ObservableRecipient, INavigationAware
{
private StorageFolder? _folder;
+ private string? _sourceFilePath;
+ private readonly ILocalSettingsService _localSettingsService;
+
public ObservableCollection Previews { get; } = [];
@@ -25,10 +30,19 @@ public partial class MultiViewModel : ObservableRecipient, INavigationAware
public SizesControl? SizesControl { get; set; }
private bool folderLoadCancelled = false;
+ private bool _skipPreCheck = false;
+ private HashSet _excludedExtensions = [];
+
+ private const int PreCheckFileCountThreshold = 100;
+ private const int PreCheckLargeFileCountThreshold = 5;
+ private const ulong PreCheckLargeFileSizeBytes = 5 * 1024 * 1024; // 5 MB
[ObservableProperty]
public partial int FileLoadProgress { get; set; } = 0;
+ [ObservableProperty]
+ public partial bool IsCheckerBackgroundVisible { get; set; } = false;
+
[ObservableProperty]
public partial bool LoadingImages { get; set; } = false;
@@ -41,6 +55,9 @@ public partial class MultiViewModel : ObservableRecipient, INavigationAware
[ObservableProperty]
public partial int CurrentImageRendering { get; set; } = 0;
+ [ObservableProperty]
+ public partial bool IsAssessingFolder { get; set; } = false;
+
[ObservableProperty]
public partial bool ArePreviewsZoomed { get; set; } = false;
@@ -59,6 +76,35 @@ public partial class MultiViewModel : ObservableRecipient, INavigationAware
[ObservableProperty]
public partial int SizesGenerating { get; set; } = 0;
+ [ObservableProperty]
+ public partial bool IsFromDllExtraction { get; set; } = false;
+
+ [ObservableProperty]
+ public partial bool IsFolderMode { get; set; } = true;
+
+ [ObservableProperty]
+ public partial string FilesDescriptionSuffix { get; set; } = "\u00A0image files in the folder.";
+
+ [ObservableProperty]
+ public partial string CloseButtonText { get; set; } = "Close Folder";
+
+ [ObservableProperty]
+ public partial string OpenSourceTooltip { get; set; } = "Open folder...";
+
+ partial void OnIsCheckerBackgroundVisibleChanged(bool value)
+ {
+ if (Previews is null || Previews.Count == 0)
+ return;
+
+ foreach (PreviewStack stack in Previews)
+ {
+ stack.ShowCheckerBackground = value;
+ stack.UpdateSizeAndZoom();
+ }
+
+ _localSettingsService.SaveSettingAsync(nameof(IsCheckerBackgroundVisible), value);
+ }
+
[RelayCommand]
public void GoBack()
{
@@ -84,6 +130,13 @@ public async Task EditIconSizes()
}
}
+ [RelayCommand]
+ public void CancelLoading()
+ {
+ folderLoadCancelled = true;
+
+ }
+
[RelayCommand]
public async Task RegenPreviews()
{
@@ -175,12 +228,14 @@ public async Task ReloadFiles()
[RelayCommand]
public async Task OpenFolder()
{
- if (_folder is null)
- return;
+ string? targetPath = IsFromDllExtraction && _sourceFilePath is not null
+ ? Path.GetDirectoryName(_sourceFilePath)
+ : _folder?.Path;
- string outputDirectory = _folder.Path;
+ if (string.IsNullOrEmpty(targetPath))
+ return;
- Uri uri = new(outputDirectory);
+ Uri uri = new(targetPath);
LauncherOptions options = new()
{
TreatAsUntrusted = false,
@@ -195,9 +250,10 @@ private INavigationService NavigationService
get;
}
- public MultiViewModel(INavigationService navigationService)
+ public MultiViewModel(INavigationService navigationService, ILocalSettingsService localSettingsService)
{
NavigationService = navigationService;
+ _localSettingsService = localSettingsService;
}
public void OnNavigatedFrom()
@@ -207,12 +263,38 @@ public void OnNavigatedFrom()
public async void OnNavigatedTo(object parameter)
{
- if (parameter is StorageFolder folder)
+ if (parameter is MultiPageParameter navParam)
+ {
+ _folder = navParam.Folder;
+ IsFromDllExtraction = navParam.IsFromDllExtraction;
+ IsFolderMode = !IsFromDllExtraction;
+ _sourceFilePath = navParam.SourceFilePath;
+ }
+ else if (parameter is StorageFolder folder)
+ {
_folder = folder;
+ }
+
+ if (IsFromDllExtraction && _sourceFilePath is not null)
+ {
+ FolderName = Path.GetFileName(_sourceFilePath);
+ FilesDescriptionSuffix = "\u00A0icons extracted.";
+ CloseButtonText = "Close";
+ OpenSourceTooltip = "Open containing folder...";
+ }
+ else
+ {
+ string path = _folder?.Path ?? "Folder path";
+ FolderName = path.Length > 50
+ ? string.Concat(path.AsSpan(0, 20), "...", path.AsSpan(path.Length - 20))
+ : path;
+ FilesDescriptionSuffix = "\u00A0image files in the folder.";
+ CloseButtonText = "Close Folder";
+ OpenSourceTooltip = "Open folder...";
+ }
- FolderName = _folder?.Path ?? "Folder path";
- if (FolderName.Length > 50) // truncate the text from the middle
- FolderName = string.Concat(FolderName.AsSpan(0, 20), "...", FolderName.AsSpan(FolderName.Length - 20));
+ _skipPreCheck = await _localSettingsService
+ .ReadSettingAsync(PreCheckDialog.SkipPreCheckSettingKey);
LoadIconSizes();
await LoadFiles();
@@ -272,12 +354,42 @@ private void CheckIfRefreshIsNeeded()
SizesControl.ViewModel.SizeDisabledWarningIsOpen = smallestSource < largestSize;
}
+ private static async Task> BuildFileGroupsAsync(
+ IReadOnlyList files)
+ {
+ StorageFile[] imageFiles = [.. files.Where(f => f.IsSupportedImageFormat())];
+ Task[] propTasks =
+ [.. imageFiles.Select(f => f.GetBasicPropertiesAsync().AsTask())];
+ Windows.Storage.FileProperties.BasicProperties[] allProps =
+ await Task.WhenAll(propTasks);
+
+ Dictionary groups = [];
+ for (int i = 0; i < imageFiles.Length; i++)
+ {
+ string ext = imageFiles[i].FileType.ToLowerInvariant();
+ if (!groups.TryGetValue(ext, out FileGroupItem? group))
+ {
+ group = new FileGroupItem { Extension = ext };
+ groups[ext] = group;
+ }
+ group.TotalCount++;
+ if (allProps[i].Size > PreCheckLargeFileSizeBytes)
+ group.LargeFileCount++;
+ }
+ return [.. groups.Values.OrderBy(g => g.Extension)];
+ }
+
+ private static bool ShouldShowPreCheck(List groups)
+ => groups.Sum(g => g.TotalCount) > PreCheckFileCountThreshold
+ || groups.Sum(g => g.LargeFileCount) > PreCheckLargeFileCountThreshold;
+
private async Task LoadFiles()
{
if (_folder is null || SizesControl == null)
return;
LoadingImages = true;
+ IsAssessingFolder = true;
Previews.Clear();
Progress progress = new();
@@ -287,6 +399,49 @@ private async Task LoadFiles()
FileLoadProgress = 0;
CurrentImageRendering = 0;
+ IsAssessingFolder = false;
+ _excludedExtensions = [];
+ bool dialogWasShown = false;
+
+ if (!_skipPreCheck)
+ {
+ List groups = await BuildFileGroupsAsync(tempFiles);
+
+ if (ShouldShowPreCheck(groups))
+ {
+ // Seed .ico default from the current SkipIcoFiles preference
+ FileGroupItem? icoGroup = groups.FirstOrDefault(g =>
+ g.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase));
+ icoGroup?.IsIncluded = !SkipIcoFiles;
+
+ PreCheckDialog dialog = new() { TotalImageCount = NumberOfImageFiles };
+ foreach (FileGroupItem group in groups)
+ dialog.FileGroups.Add(group);
+
+ _ = await NavigationService.ShowModal(dialog);
+ dialogWasShown = true;
+
+ if (!dialog.IsConfirmed)
+ {
+ LoadingImages = false;
+ GoBack();
+ return;
+ }
+
+ _excludedExtensions = [.. dialog.FileGroups
+ .Where(g => !g.IsIncluded)
+ .Select(g => g.Extension)];
+ }
+ }
+
+ // If the dialog was not shown, honour SkipIcoFiles via the exclusion set
+ if (!dialogWasShown && SkipIcoFiles)
+ _excludedExtensions = [.. _excludedExtensions.Append(".ico")];
+
+ // Recalculate total to match what will actually be processed
+ NumberOfImageFiles = tempFiles.Count(f =>
+ f.IsSupportedImageFormat() &&
+ !_excludedExtensions.Contains(f.FileType.ToLowerInvariant()));
List sizes = [.. SizesControl.ViewModel.IconSizes.Where(x => x.IsSelected && x.IsEnabled && !x.IsHidden)];
@@ -299,17 +454,18 @@ private async Task LoadFiles()
if (!file.IsSupportedImageFormat() || folderLoadCancelled)
continue;
- CurrentImageRendering++;
-
- if (SkipIcoFiles && file.FileType == ".ico")
+ if (_excludedExtensions.Contains(file.FileType.ToLowerInvariant()))
continue;
+ CurrentImageRendering++;
+
PreviewStack preview = new(file.Path, sizes, true)
{
MaxWidth = 600,
MinWidth = 300,
Margin = new Thickness(6),
- SortOrder = sortOrder
+ SortOrder = sortOrder,
+ ShowCheckerBackground = IsCheckerBackgroundVisible
};
Previews.Add(preview);
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml
index a6cd53b..2291193 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml
@@ -12,18 +12,47 @@
Padding="80,20,80,20"
HorizontalAlignment="Stretch"
Spacing="12">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ● Extract icons from EXE and DLL files
+ ● Checker background behind image previews
+ ● Improved navigation with TitleBar back button
+ ● Bug fixes
+
+
+
+
+
+
+
+ ● AI-powered background removal
+ ● Windows Share Target support
+ ● Improved save file picker naming
+ ● Update packages
+ ● Bug fixes
+
+
+
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml.cs
index e4026c1..f07e5a1 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/AboutPage.xaml.cs
@@ -1,6 +1,9 @@
-using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
using Simple_Icon_File_Maker.ViewModels;
using Windows.ApplicationModel;
+using Windows.System;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -16,6 +19,31 @@ public AboutPage()
InitializeComponent();
ViewModel = App.GetService();
VersionNumber.Text = GetAppDescription();
+ InitializeThemeSelection();
+ KeyDown += AboutPage_KeyDown;
+ }
+
+ private void AboutPage_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == VirtualKey.Escape)
+ ViewModel.GoBack();
+ }
+
+ private void InitializeThemeSelection()
+ {
+ Microsoft.UI.Xaml.ElementTheme currentTheme = ViewModel.Theme;
+ DefaultThemeButton.IsChecked = currentTheme == Microsoft.UI.Xaml.ElementTheme.Default;
+ LightThemeButton.IsChecked = currentTheme == Microsoft.UI.Xaml.ElementTheme.Light;
+ DarkThemeButton.IsChecked = currentTheme == Microsoft.UI.Xaml.ElementTheme.Dark;
+ }
+
+ private async void ThemeButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is RadioButton button &&
+ Enum.TryParse(button.Tag?.ToString(), out Microsoft.UI.Xaml.ElementTheme theme))
+ {
+ await ViewModel.SwitchThemeAsync(theme);
+ }
}
private static string GetAppDescription()
@@ -30,6 +58,57 @@ private static string GetAppDescription()
private async void ReviewBTN_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
// NavigateUri="ms-windows-store://review/?ProductId=9NS1BM1FB99Z"
- bool result = await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-windows-store://review/?ProductId=9NS1BM1FB99Z"));
+ await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-windows-store://review/?ProductId=9NS1BM1FB99Z"));
+ }
+
+ private async void AppRatingControl_ValueChanged(RatingControl sender, object args)
+ {
+ double rating = sender.Value;
+ sender.Value = -1;
+
+ if (rating <= 0)
+ return;
+
+ if (rating >= 4)
+ {
+ await Windows.System.Launcher.LaunchUriAsync(new Uri("ms-windows-store://review/?ProductId=9NS1BM1FB99Z"));
+ return;
+ }
+
+ TextBox feedbackBox = new()
+ {
+ PlaceholderText = "Describe the issue or suggestion...",
+ AcceptsReturn = true,
+ Height = 120,
+ TextWrapping = TextWrapping.Wrap,
+ };
+
+ StackPanel content = new() { Spacing = 8 };
+ content.Children.Add(new TextBlock
+ {
+ Text = "We're sorry to hear that! Please describe any issues or suggestions:",
+ TextWrapping = TextWrapping.Wrap,
+ });
+ content.Children.Add(feedbackBox);
+
+ ContentDialog dialog = new()
+ {
+ Title = "Send Feedback",
+ Content = content,
+ PrimaryButtonText = "Send Email",
+ CloseButtonText = "Cancel",
+ DefaultButton = ContentDialogButton.Primary,
+ XamlRoot = XamlRoot,
+ };
+
+ ContentDialogResult result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary)
+ {
+ string subject = Uri.EscapeDataString("Simple Icon File Maker Feedback");
+ string body = Uri.EscapeDataString(feedbackBox.Text);
+ await Windows.System.Launcher.LaunchUriAsync(
+ new Uri($"mailto:joe@joefinapps.com?subject={subject}&body={body}"));
+ }
}
}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/CropImageDialog.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/CropImageDialog.xaml
new file mode 100644
index 0000000..6f37953
--- /dev/null
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/CropImageDialog.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/CropImageDialog.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/Views/CropImageDialog.xaml.cs
new file mode 100644
index 0000000..4d5e59c
--- /dev/null
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/CropImageDialog.xaml.cs
@@ -0,0 +1,72 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Storage;
+using Windows.Storage.Streams;
+
+namespace Simple_Icon_File_Maker;
+
+public sealed partial class CropImageDialog : ContentDialog
+{
+ public string? ResultImagePath { get; private set; }
+
+ private readonly string _imagePath;
+
+ public CropImageDialog(string imagePath)
+ {
+ InitializeComponent();
+ _imagePath = imagePath;
+ PrimaryButtonClick += OnPrimaryButtonClick;
+ }
+
+ private async void ContentDialog_Loaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ StorageFile file = await StorageFile.GetFileFromPathAsync(_imagePath);
+ await ImageCropperControl.LoadImageFromFile(file);
+
+ LoadingRing.IsActive = false;
+ LoadingRing.Visibility = Visibility.Collapsed;
+ ImageCropperControl.Visibility = Visibility.Visible;
+ IsPrimaryButtonEnabled = true;
+ }
+ catch (Exception ex)
+ {
+ LoadingRing.IsActive = false;
+ LoadingRing.Visibility = Visibility.Collapsed;
+ ErrorInfoBar.Title = "Error";
+ ErrorInfoBar.Message = $"Failed to load image: {ex.Message}";
+ ErrorInfoBar.IsOpen = true;
+ }
+ }
+
+ private async void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ ContentDialogButtonClickDeferral deferral = args.GetDeferral();
+
+ try
+ {
+ StorageFolder cacheFolder = ApplicationData.Current.LocalCacheFolder;
+ string fileName = Path.GetFileNameWithoutExtension(_imagePath);
+ string newFileName = $"{fileName}_crop.png";
+
+ StorageFile outputFile = await cacheFolder.CreateFileAsync(newFileName, CreationCollisionOption.ReplaceExisting);
+
+ using IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite);
+ await ImageCropperControl.SaveAsync(stream, CommunityToolkit.WinUI.Controls.BitmapFileFormat.Png);
+
+ ResultImagePath = outputFile.Path;
+ }
+ catch (Exception ex)
+ {
+ ErrorInfoBar.Title = "Error";
+ ErrorInfoBar.Message = $"Failed to save cropped image: {ex.Message}";
+ ErrorInfoBar.IsOpen = true;
+ args.Cancel = true;
+ }
+ finally
+ {
+ deferral.Complete();
+ }
+ }
+}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml
index 43da06d..a7dbf4b 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/MainPage.xaml
@@ -10,6 +10,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:Simple_Icon_File_Maker.Models"
xmlns:toolkitConverters="using:CommunityToolkit.WinUI.Converters"
+ NavigationCacheMode="Required"
SizeChanged="Page_SizeChanged"
mc:Ignorable="d">
@@ -69,7 +70,10 @@
MinWidth="320"
MinHeight="220"
Margin="24,24,24,0"
- Visibility="{x:Bind ViewModel.IsImageSelected, Converter={StaticResource BoolVis}, ConverterParameter=True, Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.IsImageSelected,
+ Converter={StaticResource BoolVis},
+ ConverterParameter=True,
+ Mode=OneWay}">
+ ToolTipService.ToolTip="(Pro) Open a folder of images
Right-click for more options">
+
+
+
+
+
+
+
+
+
@@ -161,7 +175,9 @@
HorizontalAlignment="Center"
VerticalAlignment="Top"
Command="{x:Bind ViewModel.UpgradeToProCommand}"
- Visibility="{x:Bind ViewModel.ShowUpgradeToProButton, Converter={StaticResource BoolVis}, Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.ShowUpgradeToProButton,
+ Converter={StaticResource BoolVis},
+ Mode=OneWay}">
@@ -178,7 +194,8 @@
Visibility="Collapsed">
@@ -189,7 +206,9 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12"
- Visibility="{x:Bind ViewModel.IsLoading, Converter={StaticResource BoolVis}, Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.IsLoading,
+ Converter={StaticResource BoolVis},
+ Mode=OneWay}">
+ Value="{x:Bind ViewModel.LoadProgress,
+ Mode=OneWay}" />
+ Visibility="{x:Bind ViewModel.IsImageSelected,
+ Converter={StaticResource BoolVis},
+ Mode=OneWay}">
@@ -221,7 +243,9 @@
HorizontalAlignment="Left"
VerticalAlignment="Top"
Command="{x:Bind ViewModel.UpgradeToProCommand}"
- Visibility="{x:Bind ViewModel.ShowUpgradeToProButton, Converter={StaticResource BoolVis}, Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.ShowUpgradeToProButton,
+ Converter={StaticResource BoolVis},
+ Mode=OneWay}">
@@ -244,10 +268,10 @@
Height="32"
Command="{x:Bind ViewModel.CheckForProEditColorCommand}"
IsEnabled="True"
- ToolTipService.ToolTip="Change color of the image">
+ ToolTipService.ToolTip="Edit the source image">
-
+
+
@@ -332,7 +364,9 @@
x:Name="SettingsCard"
Grid.Row="2"
Style="{StaticResource CardStyle}"
- Visibility="{x:Bind ViewModel.IsImageSelected, Converter={StaticResource BoolVis}, Mode=OneWay}">
+ Visibility="{x:Bind ViewModel.IsImageSelected,
+ Converter={StaticResource BoolVis},
+ Mode=OneWay}">
@@ -360,11 +394,14 @@
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="16"
ToolTipService.ToolTip="Open Destination Folder..."
- Visibility="{x:Bind ViewModel.OpenFolderButtonVisible, Converter={StaticResource BoolVis}, Mode=OneWay}" />
+ Visibility="{x:Bind ViewModel.OpenFolderButtonVisible,
+ Converter={StaticResource BoolVis},
+ Mode=OneWay}" />
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/MultiPage.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/MultiPage.xaml
index b623f79..67133ad 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Views/MultiPage.xaml
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/MultiPage.xaml
@@ -45,11 +45,6 @@
-
-
-
-
-
@@ -58,17 +53,24 @@
-
+
-
+
+
+ ToolTipService.ToolTip="{x:Bind ViewModel.OpenSourceTooltip,
+ Mode=OneWay}">
-
+
-
+
@@ -95,15 +97,14 @@
+ ToolTipService.ToolTip="Return to start page">
-
+
@@ -129,7 +130,9 @@
@@ -201,7 +204,9 @@
+ Style="{x:Bind ViewModel.IsRefreshNeeded,
+ Mode=OneWay,
+ Converter={StaticResource IsPrimary}}">
-
+ Margin="0,0,8,0"
+ HorizontalAlignment="Right"
+ Orientation="Horizontal"
+ Spacing="8">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ItemsSource="{x:Bind ViewModel.Previews,
+ Mode=OneWay}">
@@ -241,7 +275,9 @@
+ Visibility="{x:Bind ViewModel.LoadingImages,
+ Mode=OneWay,
+ Converter={StaticResource BoolVis}}">
+ IsIndeterminate="{x:Bind ViewModel.IsAssessingFolder,
+ Mode=OneWay}"
+ Value="{x:Bind ViewModel.FileLoadProgress,
+ Mode=OneWay}" />
-
-
-
-
-
+ Spacing="4">
+
+
+
+
+
+
+
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/PreCheckDialog.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/PreCheckDialog.xaml
new file mode 100644
index 0000000..d58e05a
--- /dev/null
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/PreCheckDialog.xaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ File groups found:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/PreCheckDialog.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/Views/PreCheckDialog.xaml.cs
new file mode 100644
index 0000000..2c90f1c
--- /dev/null
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/PreCheckDialog.xaml.cs
@@ -0,0 +1,43 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Simple_Icon_File_Maker.Contracts.Services;
+using Simple_Icon_File_Maker.Models;
+using System.Collections.ObjectModel;
+
+namespace Simple_Icon_File_Maker;
+
+public sealed partial class PreCheckDialog : ContentDialog
+{
+ public const string SkipPreCheckSettingKey = "SkipPreCheckDialog";
+
+ // Set by caller before ShowModal
+ public int TotalImageCount { get; set; }
+ public ObservableCollection FileGroups { get; } = [];
+
+ // Read by caller after ShowModal returns
+ public bool IsConfirmed { get; private set; }
+
+ public PreCheckDialog()
+ {
+ InitializeComponent();
+ }
+
+ private void ContentDialog_Loaded(object sender, RoutedEventArgs e)
+ {
+ SummaryTextBlock.Text =
+ $"{TotalImageCount} image file{(TotalImageCount == 1 ? "" : "s")} found in this folder.";
+ }
+
+ private async void ContentDialog_PrimaryButtonClick(
+ ContentDialog sender,
+ ContentDialogButtonClickEventArgs args)
+ {
+ IsConfirmed = true;
+
+ if (DontShowAgainCheckBox.IsChecked == true)
+ {
+ await App.GetService()
+ .SaveSettingAsync(SkipPreCheckSettingKey, true);
+ }
+ }
+}
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml b/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml
index 9ca1056..0c3cb9c 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml
@@ -14,7 +14,11 @@
-
+
diff --git a/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml.cs b/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml.cs
index 2d561d6..2185170 100644
--- a/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml.cs
+++ b/Simple Icon File Maker/Simple Icon File Maker/Views/ShellPage.xaml.cs
@@ -25,4 +25,9 @@ public ShellPage(ShellViewModel viewModel)
if (App.GetService().OwnsPro)
titleBar.Subtitle += " Pro";
}
+
+ private void TitleBar_BackRequested(TitleBar sender, object args)
+ {
+ ViewModel.BackCommand.Execute(null);
+ }
}