Browse Source

Batch resize - Display input text log, bug fixes, UI/UX additions #165

Ruben 10 months ago
parent
commit
66951e920e

+ 11 - 8
src/PicView.Avalonia/Views/BatchResizeView.axaml

@@ -18,7 +18,7 @@
     </UserControl.Resources>
     <StackPanel>
 
-        <StackPanel>
+        <StackPanel x:Name="InputStackPanel">
 
             <StackPanel Margin="0,15,0,5" Orientation="Horizontal">
                 <TextBlock
@@ -79,6 +79,7 @@
                     Margin="11,0,10,0"
                     Padding="5,7,0,7"
                     SelectedIndex="0"
+                    SelectedItem="NoConversion"
                     Width="195"
                     x:Name="ConversionComboBox">
                     <ComboBoxItem Content="{CompiledBinding NoConversion, Mode=OneWay}" x:Name="NoConversion" />
@@ -108,6 +109,7 @@
                     Margin="11,0,10,0"
                     Padding="5,7,0,7"
                     SelectedIndex="0"
+                    SelectedItem="Lossless"
                     Width="195"
                     x:Name="CompressionComboBox">
                     <ComboBoxItem Content="{CompiledBinding Lossless, Mode=OneWay}" x:Name="Lossless" />
@@ -165,6 +167,7 @@
                     Margin="11,0,10,0"
                     Padding="5,7,0,7"
                     SelectedIndex="0"
+                    SelectedItem="NoResizeBox"
                     Width="195"
                     x:Name="ResizeComboBox">
                     <ComboBoxItem Content="{CompiledBinding NoResize}" x:Name="NoResizeBox" />
@@ -416,9 +419,10 @@
                     Margin="11,0,10,0"
                     Padding="5,7,0,7"
                     SelectedIndex="0"
+                    SelectedItem="NoThumbnailsItem"
                     Width="195"
                     x:Name="ThumbnailsComboBox">
-                    <ComboBoxItem Content="{CompiledBinding None}" />
+                    <ComboBoxItem Content="{CompiledBinding None}" x:Name="NoThumbnailsItem" />
                     <ComboBoxItem Content="1" />
                     <ComboBoxItem Content="2" />
                     <ComboBoxItem Content="3" />
@@ -920,10 +924,6 @@
             </customControls:AutoScrollViewer>
         </StackPanel>
 
-
-
-
-
         <Border
             Background="{DynamicResource TertiaryBackgroundColor}"
             BorderBrush="{DynamicResource MainBorderColor}"
@@ -931,8 +931,11 @@
             CornerRadius="4"
             Effect="{StaticResource MenuShadowButtonBorder}"
             Margin="5,10">
-            <customControls:AutoScrollViewer Height="200">
-                <StackPanel />
+            <customControls:AutoScrollViewer
+                Height="200"
+                Padding="12,7"
+                Width="608">
+                <StackPanel x:Name="BatchLogContainer" />
             </customControls:AutoScrollViewer>
         </Border>
 

+ 167 - 27
src/PicView.Avalonia/Views/BatchResizeView.axaml.cs

@@ -1,12 +1,17 @@
-using Avalonia.Controls;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Media;
 using Avalonia.Threading;
 using ImageMagick;
 using PicView.Avalonia.FileSystem;
 using PicView.Avalonia.Navigation;
 using PicView.Avalonia.ViewModels;
+using PicView.Core.Extensions;
 using PicView.Core.FileHandling;
 using PicView.Core.ImageDecoding;
 using PicView.Core.Localization;
+using PicView.Core.Navigation;
 
 namespace PicView.Avalonia.Views;
 
@@ -14,7 +19,7 @@ public partial class BatchResizeView : UserControl
 {
     private bool _isKeepingAspectRatio = true;
     private bool _isRunning;
-    
+
     private CancellationTokenSource? _cancellationTokenSource;
 
     public BatchResizeView()
@@ -93,7 +98,7 @@ public partial class BatchResizeView : UserControl
             SourceFolderTextBox.Text = vm.FileInfo?.DirectoryName ?? string.Empty;
         };
     }
-    
+
     private void ToggleAspectRatio()
     {
         _isKeepingAspectRatio = !_isKeepingAspectRatio;
@@ -103,9 +108,57 @@ public partial class BatchResizeView : UserControl
 
     private void Reset()
     {
+        _isKeepingAspectRatio = true;
+        LinkChainImage.IsVisible = true;
+        UnlinkChainImage.IsVisible = false;
+
+        ResetProgress();
+
+        ConversionComboBox.SelectedIndex = 0;
+        ConversionComboBox.SelectedItem = NoConversion;
+
+        CompressionComboBox.SelectedIndex = 0;
+        CompressionComboBox.SelectedItem = Lossless;
+
+        IsQualityEnabledBox.IsChecked = false;
+        QualitySlider.Value = 75;
+
+        ResizeComboBox.SelectedIndex = 0;
+        ResizeComboBox.SelectedItem = NoResizeBox;
+        
+        ThumbnailsComboBox.SelectedIndex = 0;
+        ThumbnailsComboBox.SelectedItem = NoThumbnailsItem;
         
+        BatchLogContainer.Children.Clear();
+
+        if (DataContext is not MainViewModel vm)
+        {
+            return;
+        }
+
+        if (!NavigationHelper.CanNavigate(vm))
+        {
+            return;
+        }
+
+        SourceFolderTextBox.Text = vm.FileInfo?.DirectoryName ?? string.Empty;
     }
-    
+
+    private void ResetProgress()
+    {
+        ProgressBar.Value = 0;
+        _isRunning = false;
+
+        StartButton.IsEnabled = true;
+
+        CancelButtonTextBlock.Text = TranslationHelper.Translation.Reset;
+        CancelButton.Classes.Remove("errorHover");
+        CancelButton.Classes.Add("altHover");
+        
+        InputStackPanel.Opacity = 1;
+        InputStackPanel.IsHitTestVisible = true;
+    }
+
     private async Task CancelBatchResize()
     {
         await _cancellationTokenSource?.CancelAsync();
@@ -113,7 +166,6 @@ public partial class BatchResizeView : UserControl
         StartButton.IsEnabled = true;
         _isRunning = false;
         ProgressBar.Value = 0;
-        await Task.CompletedTask;
     }
 
     private async Task StartBatchResize()
@@ -121,11 +173,14 @@ public partial class BatchResizeView : UserControl
         try
         {
             _cancellationTokenSource = new CancellationTokenSource();
-            
+
             CancelButtonTextBlock.Text = TranslationHelper.Translation.Cancel;
             CancelButton.Classes.Remove("altHover");
             CancelButton.Classes.Add("errorHover");
             StartButton.IsEnabled = false;
+            
+            InputStackPanel.Opacity = 0.5;
+            InputStackPanel.IsHitTestVisible = false;
 
             _isRunning = true;
 
@@ -193,15 +248,29 @@ public partial class BatchResizeView : UserControl
                 }
             }
 
-            ProgressBar.Maximum = files.Count();
+            var enumerable = files as string[] ?? files.ToArray();
+            ProgressBar.Maximum = enumerable.Length;
             ProgressBar.Value = 0;
 
-            await Parallel.ForEachAsync(files, _cancellationTokenSource.Token, async (file, token) =>
+            var options = new ParallelOptions
+            {
+                CancellationToken = _cancellationTokenSource.Token,
+                MaxDegreeOfParallelism = Environment.ProcessorCount - 1
+            };
+
+            await Parallel.ForEachAsync(enumerable, options, async (file, token) =>
             {
                 token.ThrowIfCancellationRequested();
-                
+
                 var ext = Path.GetExtension(file);
                 var destination = Path.Combine(outputFolder, Path.GetFileName(file));
+                
+                var fileInfo = new FileInfo(file);
+                
+                using var magick = new MagickImage();
+                magick.Ping(file);
+                
+                var oldSize = $" ({magick.Width} x {magick.Height}{ImageTitleFormatter.FormatAspectRatio((int)magick.Width, (int)magick.Height)}{fileInfo.Length.GetReadableFileSize()}";
 
                 if (toConvert)
                 {
@@ -233,8 +302,11 @@ public partial class BatchResizeView : UserControl
                     }
                 }
 
-                var success = await SaveImageFileHelper.SaveImageAsync(null,
-                    file,
+                await using var stream = FileHelper.GetOptimizedFileStream(fileInfo);
+
+                var success = await SaveImageFileHelper.SaveImageAsync(
+                    stream,
+                    null,
                     destination,
                     width,
                     height,
@@ -248,9 +320,22 @@ public partial class BatchResizeView : UserControl
 
                 if (success)
                 {
-                    await ProcessThumbs(file, Path.GetDirectoryName(destination), quality, ext).ConfigureAwait(false);
+                    using var newMagick = new MagickImage();
+                    newMagick.Ping(destination);
+                    var newFileInfo = new FileInfo(destination);
+                    
+                    var newSize = $" ({newMagick.Width} x {newMagick.Height}{ImageTitleFormatter.FormatAspectRatio((int)newMagick.Width, (int)newMagick.Height)}{newFileInfo.Length.GetReadableFileSize()}";
 
-                    await Dispatcher.UIThread.InvokeAsync(() => { ProgressBar.Value++; });
+                    await Dispatcher.UIThread.InvokeAsync(() =>
+                    {
+                        BatchLogContainer.Children.Add(CreateTextBlockLog(Path.GetFileName(file), oldSize,
+                            newSize));
+                    });
+                    await ProcessThumbs(file, Path.GetDirectoryName(destination), quality, ext).ConfigureAwait(false);
+                    await Dispatcher.UIThread.InvokeAsync(() =>
+                    {
+                        ProgressBar.Value++;
+                    });
                 }
             }).ConfigureAwait(false);
 
@@ -337,8 +422,19 @@ public partial class BatchResizeView : UserControl
                         }
                     });
 
-                    var success = await SaveImageFileHelper.SaveImageAsync(null,
-                        file,
+                    var fileInfo = new FileInfo(file);
+                
+                    using var magick = new MagickImage();
+                    magick.Ping(file);
+                
+                    var oldSize = $" ({magick.Width} x {magick.Height}{ImageTitleFormatter.FormatAspectRatio((int)magick.Width, (int)magick.Height)}{fileInfo.Length.GetReadableFileSize()}";
+
+                    await using var stream = FileHelper.GetOptimizedFileStream(fileInfo);
+
+                    _cancellationTokenSource.Token.ThrowIfCancellationRequested();
+                    
+                    var success = await SaveImageFileHelper.SaveImageAsync(stream,
+                        null,
                         destination,
                         thumbWidth,
                         thumbHeight,
@@ -352,6 +448,18 @@ public partial class BatchResizeView : UserControl
 
                     if (success)
                     {
+                        using var newMagick = new MagickImage();
+                        newMagick.Ping(destination);
+                        var newFileInfo = new FileInfo(destination);
+                    
+                        var newSize = $" ({newMagick.Width} x {newMagick.Height}{ImageTitleFormatter.FormatAspectRatio((int)newMagick.Width, (int)newMagick.Height)}{newFileInfo.Length.GetReadableFileSize()}";
+
+                        await Dispatcher.UIThread.InvokeAsync(() =>
+                        {
+                            _cancellationTokenSource.Token.ThrowIfCancellationRequested();
+                            BatchLogContainer.Children.Add(CreateTextBlockLog(Path.GetFileName(file), oldSize,
+                                newSize));
+                        });
                     }
                 }
             }
@@ -364,18 +472,8 @@ public partial class BatchResizeView : UserControl
         }
         finally
         {
-            await Dispatcher.UIThread.InvokeAsync(() =>
-            {
-                ProgressBar.Value = 0;
-                _isRunning = false;
-                
-                StartButton.IsEnabled = true;
-                
-                CancelButtonTextBlock.Text = TranslationHelper.Translation.Reset;
-                CancelButton.Classes.Remove("errorHover");
-                CancelButton.Classes.Add("altHover");
-            });
-            
+            await Dispatcher.UIThread.InvokeAsync(ResetProgress);
+
         }
     }
 
@@ -389,4 +487,46 @@ public partial class BatchResizeView : UserControl
 
         StartButton.IsEnabled = true;
     }
+
+    private TextBlock CreateTextBlockLog(string fileName, string oldSize, string newSize)
+    {
+        var textBlock = new TextBlock
+        {
+            Classes = { "txt" },
+            Padding = new Thickness(0, 0, 0, 5),
+            MaxWidth = 580
+        };
+
+        var fileNameRun = new Run
+        {
+            Text = fileName
+        };
+
+        var oldSizeRun = new Run
+        {
+            Text = oldSize,
+            Foreground = Brushes.Red,
+            TextDecorations = TextDecorations.Strikethrough
+        };
+
+        var newSizeRun = new Run
+        {
+            Text = newSize,
+            Foreground = Brushes.Green,
+            FontFamily = new FontFamily("avares://PicView.Avalonia/Assets/Fonts/Roboto-Bold.ttf#Roboto")
+        };
+
+        textBlock.Inlines.Add(fileNameRun);
+        textBlock.Inlines.Add(oldSizeRun);
+        textBlock.Inlines.Add(newSizeRun);
+
+        return textBlock;
+    }
+
+    ~BatchResizeView()
+    {
+        _cancellationTokenSource?.Cancel();
+        _cancellationTokenSource?.Dispose();
+    }
+
 }

+ 1 - 1
src/PicView.Core/FileHandling/FileHelper.cs

@@ -157,7 +157,7 @@ public static partial class FileHelper
         // Open a FileStream with the selected buffer size and options
         return new FileStream(
             fileInfo.FullName,
-            FileMode.Open,
+            FileMode.OpenOrCreate,
             FileAccess.ReadWrite,
             FileShare.ReadWrite,
             bufferSize,

+ 48 - 47
src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs

@@ -5,32 +5,33 @@ namespace PicView.Core.ImageDecoding;
 
 public static class SaveImageFileHelper
 {
-/// <summary>
-/// Saves an image asynchronously from a stream or file path with optional resizing, rotation, format conversion, 
-/// and compression settings (lossy or lossless).
-/// </summary>
-/// <param name="stream">The stream containing the image data. If null, the image will be loaded from the specified file path.</param>
-/// <param name="path">The path of the image file to load. If null, the image will be loaded from the stream.</param>
-/// <param name="destination">The path to save the processed image. If null, the image will be saved to the original path.</param>
-/// <param name="width">The target width of the image. If null, the image will not be resized based on width.</param>
-/// <param name="height">The target height of the image. If null, the image will not be resized based on height.</param>
-/// <param name="quality">The quality level of the saved image, as a percentage (0-100). If null, the default quality is used.</param>
-/// <param name="ext">The file extension of the output image (e.g., ".jpg", ".png"). If null, the original extension is kept.</param>
-/// <param name="rotationAngle">The angle to rotate the image, in degrees. If null, no rotation is applied.</param>
-/// <param name="percentage">The percentage by which to resize the image. If specified, both width and height are ignored.</param>
-/// <param name="losslessCompress">Indicates whether to apply lossless compression to the image.</param>
-/// <param name="lossyCompress">Indicates whether to apply lossy compression to the image.</param>
-/// <param name="respectAspectRatio">Indicates whether to maintain the aspect ratio when resizing.</param>
-/// <returns>A task representing the asynchronous operation, with a result of <c>true</c> if the image is saved successfully; otherwise, <c>false</c>.</returns>
-/// <remarks>
-/// If both width and height are null or zero, the image will not be resized.
-/// If the percentage is specified, it takes precedence over width and height for resizing.
-/// If both lossy and lossless compression are enabled, only one will be applied based on supported formats.
-/// </remarks>
-public static async Task<bool> SaveImageAsync(Stream? stream, string? path, string? destination = null,
-    uint? width = null, uint? height = null, uint? quality = null, string? ext = null, double? rotationAngle = null,
-    Percentage? percentage = null, bool losslessCompress = false, bool lossyCompress = false, bool respectAspectRatio = true)
-{
+    /// <summary>
+    /// Saves an image asynchronously from a stream or file path with optional resizing, rotation, format conversion, 
+    /// and compression settings (lossy or lossless).
+    /// </summary>
+    /// <param name="stream">The stream containing the image data. If null, the image will be loaded from the specified file path.</param>
+    /// <param name="path">The path of the image file to load. If null, the image will be loaded from the stream.</param>
+    /// <param name="destination">The path to save the processed image. If null, the image will be saved to the original path.</param>
+    /// <param name="width">The target width of the image. If null, the image will not be resized based on width.</param>
+    /// <param name="height">The target height of the image. If null, the image will not be resized based on height.</param>
+    /// <param name="quality">The quality level of the saved image, as a percentage (0-100). If null, the default quality is used.</param>
+    /// <param name="ext">The file extension of the output image (e.g., ".jpg", ".png"). If null, the original extension is kept.</param>
+    /// <param name="rotationAngle">The angle to rotate the image, in degrees. If null, no rotation is applied.</param>
+    /// <param name="percentage">The percentage by which to resize the image. If specified, both width and height are ignored.</param>
+    /// <param name="losslessCompress">Indicates whether to apply lossless compression to the image.</param>
+    /// <param name="lossyCompress">Indicates whether to apply lossy compression to the image.</param>
+    /// <param name="respectAspectRatio">Indicates whether to maintain the aspect ratio when resizing.</param>
+    /// <returns>A task representing the asynchronous operation, with a result of <c>true</c> if the image is saved successfully; otherwise, <c>false</c>.</returns>
+    /// <remarks>
+    /// If both width and height are null or zero, the image will not be resized.
+    /// If the percentage is specified, it takes precedence over width and height for resizing.
+    /// If both lossy and lossless compression are enabled, only one will be applied based on supported formats.
+    /// </remarks>
+    public static async Task<bool> SaveImageAsync(Stream? stream, string? path, string? destination = null,
+        uint? width = null, uint? height = null, uint? quality = null, string? ext = null, double? rotationAngle = null,
+        Percentage? percentage = null, bool losslessCompress = false, bool lossyCompress = false,
+        bool respectAspectRatio = true)
+    {
         try
         {
             using MagickImage magickImage = new();
@@ -125,7 +126,7 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
                     _ => magickImage.Format
                 };
             }
-            
+
             if (destination is not null)
             {
                 await magickImage.WriteAsync(!keepExt ? Path.ChangeExtension(destination, ext) : destination)
@@ -142,7 +143,7 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
             {
                 return false;
             }
-            
+
             if (lossyCompress || losslessCompress)
             {
                 ImageOptimizer imageOptimizer = new()
@@ -167,25 +168,25 @@ public static async Task<bool> SaveImageAsync(Stream? stream, string? path, stri
     }
 
 
-/// <summary>
-/// Resizes and optionally compresses an image asynchronously, with optional format conversion.
-/// </summary>
-/// <param name="fileInfo">The FileInfo object representing the image file to resize.</param>
-/// <param name="width">The target width of the resized image. Ignored if percentage is specified.</param>
-/// <param name="height">The target height of the resized image. Ignored if percentage is specified.</param>
-/// <param name="quality">The quality level of the resized image, as a percentage (0-100). Defaults to 100.</param>
-/// <param name="percentage">An optional percentage to resize the image by. If specified, width and height are ignored.</param>
-/// <param name="destination">The path to save the resized image. If null, the original file will be overwritten.</param>
-/// <param name="compress">Indicates whether to apply compression to the image after resizing. If null, no compression is applied.</param>
-/// <param name="ext">The file extension of the output image (e.g., ".jpg", ".png"). If null, the original extension is kept.</param>
-/// <returns>A task representing the asynchronous operation, with a result of <c>true</c> if the image is resized and saved successfully; otherwise, <c>false</c>.</returns>
-/// <remarks>
-/// If both width and height are provided, they will be used for resizing unless a percentage is specified.
-/// Compression is only applied if the specified format supports it.
-/// </remarks>
-public static async Task<bool> ResizeImageAsync(FileInfo fileInfo, uint width, uint height, uint? quality = 100,
-    Percentage? percentage = null, string? destination = null, bool? compress = null, string? ext = null)
-{
+    /// <summary>
+    /// Resizes and optionally compresses an image asynchronously, with optional format conversion.
+    /// </summary>
+    /// <param name="fileInfo">The FileInfo object representing the image file to resize.</param>
+    /// <param name="width">The target width of the resized image. Ignored if percentage is specified.</param>
+    /// <param name="height">The target height of the resized image. Ignored if percentage is specified.</param>
+    /// <param name="quality">The quality level of the resized image, as a percentage (0-100). Defaults to 100.</param>
+    /// <param name="percentage">An optional percentage to resize the image by. If specified, width and height are ignored.</param>
+    /// <param name="destination">The path to save the resized image. If null, the original file will be overwritten.</param>
+    /// <param name="compress">Indicates whether to apply compression to the image after resizing. If null, no compression is applied.</param>
+    /// <param name="ext">The file extension of the output image (e.g., ".jpg", ".png"). If null, the original extension is kept.</param>
+    /// <returns>A task representing the asynchronous operation, with a result of <c>true</c> if the image is resized and saved successfully; otherwise, <c>false</c>.</returns>
+    /// <remarks>
+    /// If both width and height are provided, they will be used for resizing unless a percentage is specified.
+    /// Compression is only applied if the specified format supports it.
+    /// </remarks>
+    public static async Task<bool> ResizeImageAsync(FileInfo fileInfo, uint width, uint height, uint? quality = 100,
+        Percentage? percentage = null, string? destination = null, bool? compress = null, string? ext = null)
+    {
         if (fileInfo.Exists == false)
         {
             return false;

+ 1 - 1
src/PicView.Core/Navigation/ImageTitleFormatter.cs

@@ -220,7 +220,7 @@ public static class ImageTitleFormatter
     /// <param name="width">The width of the image in pixels.</param>
     /// <param name="height">The height of the image in pixels.</param>
     /// <returns>A string representing the aspect ratio in the format "x:y", or an empty string if the ratio is too large.</returns>
-    private static string FormatAspectRatio(int width, int height)
+    public static string FormatAspectRatio(int width, int height)
     {
         if (width <= 0 || height <= 0)
         {

+ 1 - 1
src/PicView.Core/PicView.Core.csproj

@@ -24,7 +24,7 @@
     <PlatformTarget>ARM64</PlatformTarget>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="14.2.0" />
+    <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="14.0.0" />
     <PackageReference Include="SharpCompress" Version="0.38.0" />
     <PackageReference Include="ZString" Version="2.6.0" />
   </ItemGroup>