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}" /> + CommandParameter="{x:Bind ZoomPreviewToggleButton.IsChecked, + Mode=OneWay}" + ToolTipService.ToolTip="Zoom in on icon previews"> + + + + + + + + + + + + + + + + + Visibility="{x:Bind ViewModel.IsLoading, + 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 @@ @@ -129,7 +130,9 @@ - + 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); + } }