浏览代码

Add Resize windows, work in progress. Update languages. Fix file duplication loading. Refactor code.

Ruben 1 年之前
父节点
当前提交
6ba5bb5fe9
共有 37 个文件被更改,包括 690 次插入70 次删除
  1. 1 3
      src/PicView.Avalonia.Win32/Views/SingleImageResizeWindow.axaml
  2. 16 10
      src/PicView.Avalonia/Navigation/ImageIterator.cs
  3. 12 0
      src/PicView.Avalonia/Navigation/NavigationHelper.cs
  4. 1 0
      src/PicView.Avalonia/Navigation/Preloader.cs
  5. 2 1
      src/PicView.Avalonia/PicViewTheme/Classes.axaml
  6. 33 0
      src/PicView.Avalonia/PicViewTheme/Controls/Button.axaml
  7. 2 1
      src/PicView.Avalonia/PicViewTheme/Controls/ComboBox.axaml
  8. 8 0
      src/PicView.Avalonia/PicViewTheme/Controls/Menu.axaml
  9. 3 0
      src/PicView.Avalonia/PicViewTheme/Controls/MenuFlyoutPresenter.axaml
  10. 76 9
      src/PicView.Avalonia/PicViewTheme/Controls/RadioButton.axaml
  11. 72 3
      src/PicView.Avalonia/PicViewTheme/Controls/SplitButton.axaml
  12. 1 1
      src/PicView.Avalonia/PicViewTheme/Controls/TextBox.axaml
  13. 3 0
      src/PicView.Avalonia/PicViewTheme/Icons.axaml
  14. 9 1
      src/PicView.Avalonia/StartUp/QuickLoad.cs
  15. 4 2
      src/PicView.Avalonia/ViewModels/MainViewModel.cs
  16. 36 0
      src/PicView.Avalonia/ViewModels/ViewModelBase.cs
  17. 8 8
      src/PicView.Avalonia/Views/ExifView.axaml
  18. 4 3
      src/PicView.Avalonia/Views/ExifView.axaml.cs
  19. 2 2
      src/PicView.Avalonia/Views/MainView.axaml
  20. 194 13
      src/PicView.Avalonia/Views/SingleImageResizeView.axaml
  21. 113 0
      src/PicView.Avalonia/Views/SingleImageResizeView.axaml.cs
  22. 2 1
      src/PicView.Avalonia/Views/UC/Menus/ImageMenu.axaml
  23. 1 0
      src/PicView.Core/Config/Languages/da.json
  24. 1 0
      src/PicView.Core/Config/Languages/de.json
  25. 1 0
      src/PicView.Core/Config/Languages/en.json
  26. 1 0
      src/PicView.Core/Config/Languages/es.json
  27. 2 1
      src/PicView.Core/Config/Languages/fr.json
  28. 1 0
      src/PicView.Core/Config/Languages/it.json
  29. 1 0
      src/PicView.Core/Config/Languages/ko.json
  30. 1 0
      src/PicView.Core/Config/Languages/pl.json
  31. 1 0
      src/PicView.Core/Config/Languages/ro.json
  32. 1 0
      src/PicView.Core/Config/Languages/ru.json
  33. 1 0
      src/PicView.Core/Config/Languages/zh-CN.json
  34. 1 0
      src/PicView.Core/Config/Languages/zh-TW.json
  35. 51 9
      src/PicView.Core/Extensions/StringExtensions.cs
  36. 22 2
      src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs
  37. 2 0
      src/PicView.Core/Localization/LanguageModel.cs

+ 1 - 3
src/PicView.Avalonia.Win32/Views/SingleImageResizeWindow.axaml

@@ -79,7 +79,7 @@
                     Height="28"
                     LineHeight="28"
                     Padding="30,0,0,0"
-                    Text="{CompiledBinding Resize,
+                    Text="{CompiledBinding ResizeImage,
                                            Mode=OneWay}"
                     TextAlignment="Center"
                     x:Name="TitleText" />
@@ -93,8 +93,6 @@
             <views:SingleImageResizeView
                 Background="{DynamicResource NoisyTexture}"
                 Focusable="True"
-                Margin="0"
-                Padding="10,2,5,10"
                 PointerPressed="MoveWindow" />
         </StackPanel>
     </Border>

+ 16 - 10
src/PicView.Avalonia/Navigation/ImageIterator.cs

@@ -30,7 +30,7 @@ public sealed class ImageIterator : IDisposable
     private PreLoader PreLoader { get; } = new();
 
     private static FileSystemWatcher? _watcher;
-    private static bool _isRunning;
+    public bool IsRunning { get; private set; }
     private readonly MainViewModel? _vm;
 
     #endregion
@@ -100,13 +100,13 @@ public sealed class ImageIterator : IDisposable
         }
 
         var retries = 0;
-        while (_isRunning && retries < 10)
+        while (IsRunning && retries < 10)
         {
             await Task.Delay(200);
             retries++;
         }
 
-        _isRunning = true;
+        IsRunning = true;
 
         var newList = await Task.FromResult(_vm.PlatformService.GetFiles(fileInfo));
         if (newList.Count == 0)
@@ -126,7 +126,7 @@ public sealed class ImageIterator : IDisposable
 
         ImagePaths = newList;
 
-        _isRunning = false;
+        IsRunning = false;
 
         var index = ImagePaths.IndexOf(e.FullPath);
         if (index < 0)
@@ -192,12 +192,12 @@ public sealed class ImageIterator : IDisposable
             return;
         }
 
-        if (_isRunning)
+        if (IsRunning)
         {
             return;
         }
 
-        _isRunning = true;
+        IsRunning = true;
         var index = ImagePaths.IndexOf(e.FullPath);
         if (index < 0)
         {
@@ -268,7 +268,7 @@ public sealed class ImageIterator : IDisposable
 
 
         FileHistoryNavigation.Remove(e.FullPath);
-        _isRunning = false;
+        IsRunning = false;
 
         SetTitleHelper.SetTitle(_vm);
         if (cleared)
@@ -289,12 +289,12 @@ public sealed class ImageIterator : IDisposable
             return;
         }
 
-        if (_isRunning)
+        if (IsRunning)
         {
             return;
         }
 
-        _isRunning = true;
+        IsRunning = true;
 
         var oldIndex = ImagePaths.IndexOf(e.OldFullPath);
         var sameFile = CurrentIndex == oldIndex;
@@ -336,7 +336,7 @@ public sealed class ImageIterator : IDisposable
 
         SetTitleHelper.SetTitle(_vm);
 
-        _isRunning = false;
+        IsRunning = false;
         FileHistoryNavigation.Rename(e.OldFullPath, e.FullPath);
         GalleryFunctions.RemoveGalleryItem(oldIndex, _vm);
         await GalleryFunctions.AddGalleryItem(index, fileInfo, _vm);
@@ -416,6 +416,12 @@ public sealed class ImageIterator : IDisposable
         InitiateFileSystemWatcher(InitialFileInfo);
     }
 
+    public async Task QuickReload()
+    {
+        RemoveCurrentItemFromPreLoader();
+        await IterateToIndex(CurrentIndex).ConfigureAwait(false);
+    }
+
     public int GetIteration(int index, NavigateTo navigateTo, bool skip1 = false)
     {
         int next;

+ 12 - 0
src/PicView.Avalonia/Navigation/NavigationHelper.cs

@@ -266,6 +266,18 @@ public static class NavigationHelper
         {
             if (fileInfo.DirectoryName == vm.ImageIterator.InitialFileInfo.DirectoryName)
             {
+                // Need to wait for the file watching to add it to the list
+                var retries = 0;
+                while (vm.ImageIterator.IsRunning && retries < 10)
+                {
+                    await Task.Delay(50).ConfigureAwait(false);
+                    retries++;
+                    if (retries > 10)
+                    {
+                        await ErrorHandling.ReloadAsync(vm);
+                        return;
+                    }
+                }
                 var index = vm.ImageIterator.ImagePaths.IndexOf(fileName);
                 if (index != -1)
                 {

+ 1 - 0
src/PicView.Avalonia/Navigation/Preloader.cs

@@ -407,6 +407,7 @@ public sealed class PreLoader : IDisposable
     {
         Dispose(true);
         GC.SuppressFinalize(this);
+        GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized, false);
     }
     
     private void Dispose(bool disposing)

+ 2 - 1
src/PicView.Avalonia/PicViewTheme/Classes.axaml

@@ -65,9 +65,10 @@
         <Setter Property="BorderBrush" Value="{DynamicResource MainBorderColor}" />
         <Setter Property="BorderThickness" Value="1" />
         <Setter Property="CornerRadius" Value="2" />
-        <Setter Property="CaretBrush" Value="{DynamicResource MainTextColor}" />
         <Setter Property="Margin" Value="5,0,0,0" />
         <Setter Property="Padding" Value="5,9,0,7" />
         <Setter Property="Width" Value="180" />
+        <Setter Property="CaretBrush" Value="{StaticResource SecondaryTextColor}" />
+        <Setter Property="SelectionForegroundBrush" Value="{StaticResource SecondaryTextColor}" />
     </Style>
 </Styles>

+ 33 - 0
src/PicView.Avalonia/PicViewTheme/Controls/Button.axaml

@@ -54,6 +54,39 @@
             <Setter Property="Background" Value="{DynamicResource AccentColor}" />
             <Setter Property="BorderBrush" Value="{DynamicResource AccentColor}" />
         </Style>
+
+        <Style Selector="^.accentHover:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+            <Style.Animations>
+                <Animation Duration="{StaticResource ButtonHoverAnimationDuration}" IterationCount="1">
+                    <KeyFrame>
+                        <Setter Property="Foreground">
+                            <SolidColorBrush Color="{StaticResource SecondaryTextColor}" />
+                        </Setter>
+                        <Setter Property="Background">
+                            <SolidColorBrush Color="{DynamicResource AccentColor}" />
+                        </Setter>
+                        <Setter Property="BorderBrush">
+                            <SolidColorBrush Color="{DynamicResource MainBorderColor}" />
+                        </Setter>
+                    </KeyFrame>
+                    <KeyFrame Cue="1">
+                        <Setter Property="Foreground">
+                            <SolidColorBrush Color="#fff" />
+                        </Setter>
+                        <Setter Property="Background">
+                            <SolidColorBrush Color="{DynamicResource SecondaryAccentColor}" />
+                        </Setter>
+                        <Setter Property="BorderBrush">
+                            <SolidColorBrush Color="{DynamicResource SecondaryAccentColor}" />
+                        </Setter>
+                    </KeyFrame>
+                </Animation>
+            </Style.Animations>
+            <Setter Property="Foreground" Value="#fff" />
+            <Setter Property="Background" Value="{DynamicResource SecondaryAccentColor}" />
+            <Setter Property="BorderBrush" Value="{DynamicResource SecondaryAccentColor}" />
+        </Style>
+
         <Style Selector="^.errorHover:pointerover /template/ ContentPresenter#PART_ContentPresenter">
             <Style.Animations>
                 <Animation Duration="{StaticResource ButtonHoverAnimationDuration}" IterationCount="1">

+ 2 - 1
src/PicView.Avalonia/PicViewTheme/Controls/ComboBox.axaml

@@ -36,7 +36,8 @@
                             Focusable="False"
                             IsChecked="{TemplateBinding IsDropDownOpen,
                                                         Mode=TwoWay}"
-                            Name="toggle">
+                            Name="toggle"
+                            Theme="{StaticResource NoneTemplate}">
                             <Panel>
                                 <ContentControl
                                     Content="{TemplateBinding SelectionBoxItem}"

+ 8 - 0
src/PicView.Avalonia/PicViewTheme/Controls/Menu.axaml

@@ -161,6 +161,14 @@
             <Setter Property="CornerRadius" Value="0,0,6,6" />
         </Style>
 
+        <Style Selector="^.noCornerRadius:nth-child(1) /template/ Border#root">
+            <Setter Property="CornerRadius" Value="0" />
+        </Style>
+
+        <Style Selector="^.noCornerRadius:nth-last-child(1) /template/ Border#root">
+            <Setter Property="CornerRadius" Value="0" />
+        </Style>
+
         <Style Selector="^:empty /template/ Path#rightArrow">
             <Setter Property="IsVisible" Value="False" />
         </Style>

+ 3 - 0
src/PicView.Avalonia/PicViewTheme/Controls/MenuFlyoutPresenter.axaml

@@ -20,5 +20,8 @@
                 </Border>
             </ControlTemplate>
         </Setter>
+        <Style Selector="^.noCornerRadius /template/ Border#LayoutRoot">
+            <Setter Property="CornerRadius" Value="0" />
+        </Style>
     </ControlTheme>
 </ResourceDictionary>

+ 76 - 9
src/PicView.Avalonia/PicViewTheme/Controls/RadioButton.axaml

@@ -1,22 +1,89 @@
 <ResourceDictionary
+    x:ClassModifier="internal"
     xmlns="https://github.com/avaloniaui"
-    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-    x:ClassModifier="internal">
-    <ControlTheme x:Key="{x:Type RadioButton}" TargetType="RadioButton">
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <ControlTheme TargetType="RadioButton" x:Key="{x:Type RadioButton}">
         <Setter Property="Background" Value="Transparent" />
-        <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
-        <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
+        <Setter Property="BorderBrush" Value="{DynamicResource MainBorderColor}" />
+        <Setter Property="BorderThickness" Value="1" />
+
+        <Setter Property="Template">
+            <ControlTemplate>
+                <Grid Background="{TemplateBinding Background}" ColumnDefinitions="Auto,*">
+                    <Border
+                        Background="{DynamicResource TertiaryBackgroundColor}"
+                        BorderBrush="{TemplateBinding BorderBrush}"
+                        BorderThickness="1"
+                        CornerRadius="6"
+                        Height="18"
+                        Name="border"
+                        VerticalAlignment="Center"
+                        Width="18" />
+                    <Ellipse
+                        Fill="#fff"
+                        Grid.Column="0"
+                        Height="10"
+                        HorizontalAlignment="Center"
+                        IsVisible="False"
+                        Name="checkMark"
+                        Stretch="Uniform"
+                        VerticalAlignment="Center"
+                        Width="10" />
+                    <Border
+                        Background="Transparent"
+                        CornerRadius="6"
+                        Grid.Column="0"
+                        Height="10"
+                        HorizontalAlignment="Center"
+                        Name="indeterminateMark"
+                        UseLayoutRounding="False"
+                        VerticalAlignment="Center"
+                        Width="10" />
+                    <ContentPresenter
+                        Content="{TemplateBinding Content}"
+                        ContentTemplate="{TemplateBinding ContentTemplate}"
+                        Grid.Column="1"
+                        Margin="6,0,0,0"
+                        Name="PART_ContentPresenter"
+                        RecognizesAccessKey="True"
+                        VerticalAlignment="Center" />
+                </Grid>
+            </ControlTemplate>
+        </Setter>
+
+        <Style Selector="^:pointerover /template/ Border#border">
+            <Setter Property="BorderBrush" Value="{DynamicResource SecondaryBorderColor}" />
+        </Style>
+        <Style Selector="^:checked /template/ Border#border">
+            <Setter Property="Background" Value="{DynamicResource AccentColor}" />
+        </Style>
+
+        <Style Selector="^ /template/ Border#indeterminateMark">
+            <Setter Property="IsVisible" Value="False" />
+        </Style>
+        <Style Selector="^:checked /template/ Ellipse#checkMark">
+            <Setter Property="IsVisible" Value="True" />
+        </Style>
+        <Style Selector="^:indeterminate /template/ Border#indeterminateMark">
+            <Setter Property="IsVisible" Value="True" />
+        </Style>
+        <Style Selector="^:disabled /template/ Border#border">
+            <Setter Property="Opacity" Value="{StaticResource ThemeDisabledOpacity}" />
+        </Style>
+    </ControlTheme>
+
+    <ControlTheme TargetType="RadioButton" x:Key="NoneTemplate">
         <Setter Property="Template">
             <ControlTemplate>
                 <Panel Background="{TemplateBinding Background}">
 
                     <ContentPresenter
-                        Name="PART_ContentPresenter"
-                        Margin="4,0,0,0"
-                        VerticalAlignment="Center"
                         Content="{TemplateBinding Content}"
                         ContentTemplate="{TemplateBinding ContentTemplate}"
-                        RecognizesAccessKey="True" />
+                        Margin="4,0,0,0"
+                        Name="PART_ContentPresenter"
+                        RecognizesAccessKey="True"
+                        VerticalAlignment="Center" />
                 </Panel>
             </ControlTemplate>
         </Setter>

+ 72 - 3
src/PicView.Avalonia/PicViewTheme/Controls/SplitButton.axaml

@@ -131,10 +131,11 @@
                         VerticalContentAlignment="Center"
                         x:Name="PART_SecondaryButton">
                         <PathIcon
-                            Data="{StaticResource ChevronUpGeometry}"
-                            Foreground="{DynamicResource MainTextColor}"
+                            Data="{StaticResource ChevronDownGeometry}"
+                            Foreground="{TemplateBinding Foreground}"
                             Height="12"
-                            Width="12" />
+                            Width="12"
+                            x:Name="PART_ChevronIcon" />
                     </Button>
                 </Grid>
             </ControlTemplate>
@@ -189,6 +190,70 @@
             <Setter Property="BorderBrush" Value="{DynamicResource SecondaryBorderColor}" />
         </Style>
 
+
+        <Style Selector="^.accentHover:pointerover /template/ Button#PART_PrimaryButton">
+            <Style.Animations>
+                <Animation Duration="{StaticResource ButtonHoverAnimationDuration}" IterationCount="1">
+                    <KeyFrame>
+                        <Setter Property="Foreground">
+                            <SolidColorBrush Color="{StaticResource SecondaryTextColor}" />
+                        </Setter>
+                        <Setter Property="Background">
+                            <SolidColorBrush Color="{DynamicResource AccentColor}" />
+                        </Setter>
+                        <Setter Property="BorderBrush">
+                            <SolidColorBrush Color="{DynamicResource MainBorderColor}" />
+                        </Setter>
+                    </KeyFrame>
+                    <KeyFrame Cue="1">
+                        <Setter Property="Foreground">
+                            <SolidColorBrush Color="#fff" />
+                        </Setter>
+                        <Setter Property="Background">
+                            <SolidColorBrush Color="{DynamicResource SecondaryAccentColor}" />
+                        </Setter>
+                        <Setter Property="BorderBrush">
+                            <SolidColorBrush Color="{DynamicResource SecondaryAccentColor}" />
+                        </Setter>
+                    </KeyFrame>
+                </Animation>
+            </Style.Animations>
+            <Setter Property="Foreground" Value="#fff" />
+            <Setter Property="Background" Value="{DynamicResource SecondaryAccentColor}" />
+            <Setter Property="BorderBrush" Value="{DynamicResource SecondaryAccentColor}" />
+        </Style>
+        <Style Selector="^.accentHover:pointerover /template/ Button#PART_SecondaryButton">
+            <Style.Animations>
+                <Animation Duration="{StaticResource ButtonHoverAnimationDuration}" IterationCount="1">
+                    <KeyFrame>
+                        <Setter Property="Foreground">
+                            <SolidColorBrush Color="{StaticResource SecondaryTextColor}" />
+                        </Setter>
+                        <Setter Property="Background">
+                            <SolidColorBrush Color="{DynamicResource AccentColor}" />
+                        </Setter>
+                        <Setter Property="BorderBrush">
+                            <SolidColorBrush Color="{DynamicResource MainBorderColor}" />
+                        </Setter>
+                    </KeyFrame>
+                    <KeyFrame Cue="1">
+                        <Setter Property="Foreground">
+                            <SolidColorBrush Color="#fff" />
+                        </Setter>
+                        <Setter Property="Background">
+                            <SolidColorBrush Color="{DynamicResource SecondaryAccentColor}" />
+                        </Setter>
+                        <Setter Property="BorderBrush">
+                            <SolidColorBrush Color="{DynamicResource SecondaryAccentColor}" />
+                        </Setter>
+                    </KeyFrame>
+                </Animation>
+            </Style.Animations>
+            <Setter Property="Foreground" Value="#fff" />
+            <Setter Property="Background" Value="{DynamicResource SecondaryAccentColor}" />
+            <Setter Property="BorderBrush" Value="{DynamicResource SecondaryAccentColor}" />
+        </Style>
+
         <Style Selector="^:flyout-open /template/ Button">
             <Setter Property="Tag" Value="flyout-open" />
         </Style>
@@ -204,5 +269,9 @@
         <Style Selector="^:disabled">
             <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
         </Style>
+
+        <Style Selector="^.up /template/ PathIcon#PART_ChevronIcon">
+            <Setter Property="Data" Value="{StaticResource ChevronUpGeometry}" />
+        </Style>
     </ControlTheme>
 </ResourceDictionary>

+ 1 - 1
src/PicView.Avalonia/PicViewTheme/Controls/TextBox.axaml

@@ -7,7 +7,7 @@
         <Setter Property="CaretBrush" Value="{DynamicResource MainTextColor}" />
         <Setter Property="SelectionBrush" Value="{DynamicResource AccentColor}" />
         <Setter Property="SelectionForegroundBrush" Value="{DynamicResource MainTextColor}" />
-        <Setter Property="Padding" Value="4" />
+        <Setter Property="Padding" Value="8,4,0,4" />
         <Setter Property="ScrollViewer.IsScrollChainingEnabled" Value="True" />
         <Setter Property="Template">
             <ControlTemplate>

文件差异内容过多而无法显示
+ 3 - 0
src/PicView.Avalonia/PicViewTheme/Icons.axaml


+ 9 - 1
src/PicView.Avalonia/Navigation/QuickLoad.cs → src/PicView.Avalonia/StartUp/QuickLoad.cs

@@ -1,6 +1,7 @@
 using Avalonia.Threading;
 using PicView.Avalonia.Gallery;
 using PicView.Avalonia.ImageHandling;
+using PicView.Avalonia.Navigation;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.WindowBehavior;
@@ -8,7 +9,7 @@ using PicView.Core.Config;
 using PicView.Core.FileHandling;
 using PicView.Core.Gallery;
 
-namespace PicView.Avalonia.Navigation;
+namespace PicView.Avalonia.StartUp;
 
 public static class QuickLoad
 {
@@ -67,6 +68,13 @@ public static class QuickLoad
         if (SettingsHelper.Settings.ImageScaling.ShowImageSideBySide)
         {
             SetTitleHelper.SetSideBySideTitle(vm, imageModel, secondaryPreloadValue?.ImageModel);
+            
+            // Sometimes the images are not rendered in side by side, this fixes it
+            // TODO: Improve and fix side by side and remove this hack 
+            Dispatcher.UIThread.Post(() =>
+            {
+                vm.ImageViewer?.MainImage?.InvalidateVisual();
+            });
         }
         else
         {

+ 4 - 2
src/PicView.Avalonia/ViewModels/MainViewModel.cs

@@ -1390,8 +1390,10 @@ public class MainViewModel : ViewModelBase
         var success = await ConversionHelper.ResizeImageByPercentage(FileInfo, percentage);
         if (success)
         {
-            ImageIterator?.RemoveCurrentItemFromPreLoader();
-            await ImageIterator?.IterateToIndex(ImageIterator.CurrentIndex);
+            if (ImageIterator is not null)
+            {
+                await ImageIterator.QuickReload().ConfigureAwait(false);
+            }
         }
         else
         {

+ 36 - 0
src/PicView.Avalonia/ViewModels/ViewModelBase.cs

@@ -231,10 +231,46 @@ public class ViewModelBase : ReactiveObject
         Center = TranslationHelper.Translation.Center;
         Tile = TranslationHelper.Translation.Tile;
         Fit = TranslationHelper.Translation.Fit;
+        Pixels = TranslationHelper.Translation.Pixels;
+        Percentage = TranslationHelper.Translation.Percentage;
+        Quality = TranslationHelper.Translation.Quality;
+        SaveAs = TranslationHelper.Translation.SaveAs;
     }
 
     #region Strings
     
+    private string? _saveAs;
+    
+    public string? SaveAs
+    {
+        get => _saveAs;
+        set => this.RaiseAndSetIfChanged(ref _saveAs, value);
+    }
+    
+    private string? _quality;
+    
+    public string? Quality
+    {
+        get => _quality;
+        set => this.RaiseAndSetIfChanged(ref _quality, value);
+    }
+    
+    private string? _percentage;
+    
+    public string? Percentage
+    {
+        get => _percentage;
+        set => this.RaiseAndSetIfChanged(ref _percentage, value);
+    }
+    
+    private string? _pixels;
+    
+    public string? Pixels
+    {
+        get => _pixels.FirstCharToUpper();
+        set => this.RaiseAndSetIfChanged(ref _pixels, value);
+    }
+    
     private string? _fit;
     
     public string? Fit

+ 8 - 8
src/PicView.Avalonia/Views/ExifView.axaml

@@ -19,9 +19,8 @@
     <UserControl.Styles>
         <Styles>
             <Style Selector="TextBox.x">
-                <Setter Property="CaretBrush" Value="{StaticResource SecondaryTextColor}" />
+                <Setter Property="Background" Value="{DynamicResource DisabledBackgroundColor}" />
                 <Setter Property="Foreground" Value="{StaticResource SecondaryTextColor}" />
-                <Setter Property="SelectionForegroundBrush" Value="{StaticResource SecondaryTextColor}" />
             </Style>
         </Styles>
     </UserControl.Styles>
@@ -44,6 +43,7 @@
                                                    Mode=OneWay}"
                             Width="100" />
                         <customControls:FuncTextBox
+                            Background="{DynamicResource SecondaryBackgroundColor}"
                             Classes="hover TStyle x"
                             Foreground="{StaticResource SecondaryTextColor}"
                             Text="{Binding FileInfo.Name, FallbackValue=''}" />
@@ -205,7 +205,7 @@
                                                    Mode=OneWay}"
                             Width="100" />
                         <customControls:FuncTextBox
-                            Classes="TStyle"
+                            Classes="TStyle x"
                             IsReadOnly="True"
                             Text="{CompiledBinding GetBitDepth,
                                                    Mode=OneWay}" />
@@ -424,7 +424,7 @@
                     </StackPanel>
                 </StackPanel>
 
-                <StackPanel Margin="15,10" Width="130">
+                <StackPanel Margin="15,10" Width="140">
                     <TextBlock
                         Classes="txt"
                         FontSize="16"
@@ -439,7 +439,7 @@
                         FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
                         Padding="5,7,0,7"
                         SelectedIndex="0"
-                        Width="130"
+                        Width="140"
                         x:Name="ConversionComboBox">
                         <ComboBoxItem Content="{CompiledBinding NoConversion, Mode=OneWay}" />
                         <ComboBoxItem Content=".png" />
@@ -457,7 +457,7 @@
                                                            ElementName=ConversionComboBox}"
                         HorizontalAlignment="Center"
                         Margin="0,12"
-                        Width="130">
+                        Width="140">
                         <TextBlock
                             Classes="txt"
                             FontSize="14"
@@ -483,7 +483,7 @@
                         Foreground="{StaticResource SecondaryTextColor}"
                         Padding="5,7,0,7"
                         SelectedIndex="0"
-                        Width="130"
+                        Width="140"
                         x:Name="PercentageComboBox">
                         <ComboBoxItem Content="{CompiledBinding NoResize, Mode=OneWay}" />
                         <ComboBoxItem Content="95%" />
@@ -514,7 +514,7 @@
                                                            ElementName=PercentageComboBox}"
                         HorizontalAlignment="Center"
                         Margin="0,12"
-                        Width="130">
+                        Width="140">
                         <TextBlock
                             Classes="txt"
                             FontSize="14"

+ 4 - 3
src/PicView.Avalonia/Views/ExifView.axaml.cs

@@ -1,7 +1,6 @@
 using Avalonia.Controls;
 using Avalonia.Input;
 using PicView.Avalonia.Converters;
-using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 
 namespace PicView.Avalonia.Views;
@@ -77,8 +76,10 @@ public partial class ExifView : UserControl
             var success = await ConversionHelper.ResizeByWidth(vm.FileInfo, width).ConfigureAwait(false);
             if (success)
             {
-                vm.ImageIterator?.RemoveCurrentItemFromPreLoader();
-                await vm.ImageIterator?.IterateToIndex(vm.ImageIterator.CurrentIndex);
+                if (vm.ImageIterator is not null)
+                {
+                    await vm.ImageIterator.QuickReload().ConfigureAwait(false);
+                }
             }
         }
     }

+ 2 - 2
src/PicView.Avalonia/Views/MainView.axaml

@@ -565,7 +565,7 @@
                     </MenuItem.Icon>
                 </MenuItem>
 
-                <MenuItem Header="{CompiledBinding ResizeImage, Mode=OneWay}" IsEnabled="False">
+                <MenuItem Command="{CompiledBinding ShowSingleImageResizeWindowCommand}" Header="{CompiledBinding ResizeImage, Mode=OneWay}">
                     <MenuItem.Icon>
                         <Image
                             Height="12"
@@ -574,7 +574,7 @@
                     </MenuItem.Icon>
                 </MenuItem>
 
-                <MenuItem Header="{CompiledBinding BatchResize, Mode=OneWay}" IsEnabled="False">
+                <MenuItem Command="{CompiledBinding ShowBatchResizeWindowCommand}" Header="{CompiledBinding BatchResize, Mode=OneWay}">
                     <MenuItem.Icon>
                         <Image
                             Height="12"

+ 194 - 13
src/PicView.Avalonia/Views/SingleImageResizeView.axaml

@@ -1,25 +1,206 @@
 <UserControl
-    ZIndex="99"
     d:DesignHeight="450"
     d:DesignWidth="800"
     mc:Ignorable="d"
     x:Class="PicView.Avalonia.Views.SingleImageResizeView"
     x:DataType="viewModels:MainViewModel"
     xmlns="https://github.com/avaloniaui"
+    xmlns:customControls="clr-namespace:PicView.Avalonia.CustomControls"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:viewModels="clr-namespace:PicView.Avalonia.ViewModels;assembly=PicView.Avalonia"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    <Panel>
-        <Border
-            BorderBrush="{DynamicResource MainBorderColor}"
-            BorderThickness="1"
-            CornerRadius="8"
-            Height="300"
-            HorizontalAlignment="Center"
-            VerticalAlignment="Center"
-            Width="300">
-            <Image Source="{CompiledBinding ImageSource}" x:Name="ImagePreview" />
-        </Border>
-    </Panel>
+    <StackPanel>
+        <StackPanel Margin="30">
+
+            <StackPanel Orientation="Horizontal">
+                <RadioButton
+                    HorizontalAlignment="Center"
+                    IsChecked="True"
+                    Width="255"
+                    x:Name="PixelsTabItem">
+                    <TextBlock
+                        Classes="txt"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        FontSize="14"
+                        Text="{CompiledBinding Pixels,
+                                               Mode=OneWay}" />
+                </RadioButton>
+                <RadioButton
+                    HorizontalAlignment="Center"
+                    IsEnabled="False"
+                    x:Name="PercentageTabItem">
+                    <TextBlock
+                        Classes="txt"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        FontSize="14"
+                        Text="{CompiledBinding Percentage,
+                                               Mode=OneWay}" />
+                </RadioButton>
+            </StackPanel>
+
+            <StackPanel
+                IsVisible="{Binding Path=IsChecked, ElementName=PixelsTabItem}"
+                Margin="0,25,0,30"
+                Orientation="Horizontal">
+                <StackPanel>
+                    <TextBlock
+                        Classes="txt"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        FontSize="14"
+                        Margin="4,0,0,5"
+                        Text="{CompiledBinding Width,
+                                               Mode=OneWay}" />
+                    <customControls:FuncTextBox
+                        Background="{DynamicResource SecondaryBackgroundColor}"
+                        Classes="hover TStyle"
+                        CornerRadius="4"
+                        Foreground="{StaticResource SecondaryTextColor}"
+                        Height="35"
+                        Margin="0"
+                        Text="{CompiledBinding PixelWidth,
+                                               Mode=OneWay}"
+                        ToolTip.Tip="{CompiledBinding SizeTooltip,
+                                                      Mode=OneWay}"
+                        Width="195"
+                        x:Name="PixelWidthTextBox" />
+                </StackPanel>
+
+                <customControls:IconButton
+                    Foreground="{StaticResource SecondaryTextColor}"
+                    Height="16"
+                    Icon="{StaticResource LinkChainImage}"
+                    IconHeight="16"
+                    IconWidth="16"
+                    IsEnabled="False"
+                    Margin="20,20,20,0"
+                    Width="16" />
+
+                <StackPanel Margin="5,0,0,0">
+                    <TextBlock
+                        Classes="txt"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        FontSize="14"
+                        Margin="4,0,0,5"
+                        Text="{CompiledBinding Height,
+                                               Mode=OneWay}" />
+                    <customControls:FuncTextBox
+                        Background="{DynamicResource SecondaryBackgroundColor}"
+                        Classes="hover TStyle"
+                        CornerRadius="4"
+                        Foreground="{StaticResource SecondaryTextColor}"
+                        Height="35"
+                        Margin="0"
+                        Text="{CompiledBinding PixelHeight,
+                                               Mode=OneWay}"
+                        ToolTip.Tip="{CompiledBinding SizeTooltip,
+                                                      Mode=OneWay}"
+                        Width="195"
+                        x:Name="PixelHeightTextBox" />
+                </StackPanel>
+            </StackPanel>
+
+            <StackPanel Orientation="Horizontal">
+                <StackPanel>
+                    <TextBlock
+                        Classes="txt"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        FontSize="14"
+                        Margin="5,0,0,2"
+                        Text="{CompiledBinding Quality,
+                                               Mode=OneWay}" />
+                    <customControls:CustomSlider
+                        IsSnapToTickEnabled="True"
+                        Margin="5,0,0,2"
+                        Maximum="100"
+                        Minimum="10"
+                        TickFrequency="1"
+                        Value="90"
+                        Width="190"
+                        x:Name="QualitySlider" />
+                    <TextBlock
+                        Classes="txt"
+                        Margin="8,0,0,3"
+                        Text="{Binding Path=Value, ElementName=QualitySlider}" />
+                </StackPanel>
+                <StackPanel Margin="60,0,0,0">
+                    <TextBlock
+                        Classes="txt"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        FontSize="14"
+                        Margin="5,0,0,5"
+                        Text="{CompiledBinding ConvertTo,
+                                               Mode=OneWay}" />
+                    <ComboBox
+                        Background="{DynamicResource SecondaryBackgroundColor}"
+                        BorderBrush="{DynamicResource MainBorderColor}"
+                        BorderThickness="1"
+                        CornerRadius="4"
+                        FontFamily="/Assets/Fonts/Roboto-Medium.ttf#Roboto"
+                        Height="30"
+                        HorizontalAlignment="Right"
+                        Margin="2,0,0,0"
+                        Padding="5,7,0,7"
+                        SelectedIndex="0"
+                        Width="195"
+                        x:Name="ConversionComboBox">
+                        <ComboBoxItem Content="{CompiledBinding NoConversion, Mode=OneWay}" x:Name="NoConversion" />
+                        <ComboBoxItem Content=".png" x:Name="PngItem" />
+                        <ComboBoxItem Content=".jpg" x:Name="JpgItem" />
+                        <ComboBoxItem Content=".webp" x:Name="WebpItem" />
+                        <ComboBoxItem Content=".avif" x:Name="AvifItem" />
+                        <ComboBoxItem Content=".heic" x:Name="HeicItem" />
+                        <ComboBoxItem Content=".jxl" x:Name="JxlItem" />
+                    </ComboBox>
+                </StackPanel>
+            </StackPanel>
+
+        </StackPanel>
+        <StackPanel
+            Background="#277A7A7A"
+            Height="80"
+            Orientation="Horizontal">
+            <Button
+                Classes="BorderStyle altHover mainBtn"
+                CornerRadius="4"
+                Margin="30,0,60,0"
+                Width="195">
+                <TextBlock
+                    Classes="txt"
+                    Text="{CompiledBinding ResetButtonText,
+                                           Mode=OneWay}"
+                    TextAlignment="Center" />
+            </Button>
+
+            <SplitButton
+                Background="{DynamicResource AccentColor}"
+                Classes="BorderStyle accentHover mainBtn"
+                CornerRadius="4"
+                Width="195"
+                x:Name="SaveButton">
+                <TextBlock
+                    Classes="txt"
+                    Text="{CompiledBinding Save,
+                                           Mode=OneWay}"
+                    TextAlignment="Center" />
+                <SplitButton.Flyout>
+                    <MenuFlyout FlyoutPresenterClasses="noCornerRadius" Placement="Bottom">
+                        <Button
+                            Background="Transparent"
+                            IsEnabled="False"
+                            Width="130"
+                            x:Name="SaveAsButton">
+                            <TextBlock
+                                Classes="txt"
+                                Foreground="{DynamicResource MainTextColor}"
+                                Text="{CompiledBinding SaveAs,
+                                                       Mode=OneWay}"
+                                TextAlignment="Center" />
+                        </Button>
+                    </MenuFlyout>
+                </SplitButton.Flyout>
+
+            </SplitButton>
+        </StackPanel>
+    </StackPanel>
 </UserControl>

+ 113 - 0
src/PicView.Avalonia/Views/SingleImageResizeView.axaml.cs

@@ -1,4 +1,7 @@
 using Avalonia.Controls;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.ViewModels;
+using PicView.Core.ImageDecoding;
 
 namespace PicView.Avalonia.Views;
 
@@ -7,5 +10,115 @@ public partial class SingleImageResizeView : UserControl
     public SingleImageResizeView()
     {
         InitializeComponent();
+        Loaded += delegate
+        {
+            if (DataContext is not MainViewModel vm)
+                return;
+            
+            SaveButton.Click += async (_, _) => await SaveImage(vm).ConfigureAwait(false);
+            SaveAsButton.Click += async (_, _) => await SaveImageAs(vm).ConfigureAwait(false);
+        };
+    }
+
+    private async Task SaveImageAs(MainViewModel vm)
+    {
+        throw new NotImplementedException();
+    }
+
+    private async Task SaveImage(MainViewModel vm)
+    {
+        await DoSaveImage(vm, vm.FileInfo.FullName).ConfigureAwait(false);
+    }
+    
+    private async Task DoSaveImage(MainViewModel vm, string destination)
+    {
+        if (!uint.TryParse(PixelWidthTextBox.Text, out var width) || !uint.TryParse(PixelHeightTextBox.Text, out var height))
+        {
+            return;
+        }
+
+        if (width == vm.PixelWidth)
+        {
+            if (height == vm.PixelHeight)
+            {
+                
+            }
+            else
+            {
+                width = 0;
+            }
+        }
+
+        if (height == vm.PixelHeight)
+        {
+            if (width == vm.PixelWidth)
+            {
+                
+            }
+            else
+            {
+                height = 0;
+            }
+        }
+
+        var file = vm.FileInfo.FullName;
+        uint? quality = null;
+        if (QualitySlider.IsEnabled)
+        {
+            quality = (uint)QualitySlider.Value;
+        }
+        var ext = vm.FileInfo.Extension;
+        if (!NoConversion.IsSelected)
+        {
+            if (PngItem.IsSelected)
+            {
+                ext = ".png";
+            }
+            else if (JpgItem.IsSelected)
+            {
+                ext = ".jpg";
+            }
+            else if (WebpItem.IsSelected)
+            {
+                ext = ".webp";
+            }
+            else if (AvifItem.IsSelected)
+            {
+                ext = ".avif";
+            }
+            else if (HeicItem.IsSelected)
+            {
+                ext = ".heic";
+            }
+            else if (JxlItem.IsSelected)
+            {
+                ext = ".jxl";
+            }
+        }
+        var success = await SaveImageFileHelper.SaveImageAsync(null, file, destination, width, height, quality, ext, vm.RotationAngle).ConfigureAwait(false);
+        if (!success)
+        {
+            // TODO: show error
+            return;
+        }
+        if (destination == file)
+        {
+            if (!NavigationHelper.CanNavigate(vm))
+            {
+                return;
+            }
+
+            if (vm.ImageIterator is not null)
+            {
+                await vm.ImageIterator.QuickReload().ConfigureAwait(false);
+            }
+        }
+        else
+        {
+            if (Path.GetDirectoryName(file) == Path.GetDirectoryName(destination))
+            {
+                await NavigationHelper.LoadPicFromFile(destination, vm).ConfigureAwait(false);
+            }
+        }
     }
 }

+ 2 - 1
src/PicView.Avalonia/Views/UC/Menus/ImageMenu.axaml

@@ -256,9 +256,10 @@
                     Background="{DynamicResource MenuButtonColor}"
                     Canvas.Left="7"
                     Canvas.Top="104"
-                    Classes="ButtonBorder altHover"
+                    Classes="ButtonBorder altHover up"
                     Command="{CompiledBinding StartSlideShowTask}"
                     CommandParameter="0"
+                    Foreground="{DynamicResource MainTextColor}"
                     Height="46"
                     ToolTip.Tip="{CompiledBinding Slideshow,
                                                   Mode=OneWay}"

+ 1 - 0
src/PicView.Core/Config/Languages/da.json

@@ -283,6 +283,7 @@
   "Rotated": "Roteret",
   "Saturation": "Farvemætning",
   "Save": "Gem",
+  "SaveAs": "Gem som",
   "SaveImage": "Gem billede",
   "SavingFileFailed": "Gemning af fil mislykkedes",
   "ScrollAndRotate": "Scroll og roter",

+ 1 - 0
src/PicView.Core/Config/Languages/de.json

@@ -283,6 +283,7 @@
   "Rotated": "Gedreht",
   "Saturation": "Sättigung",
   "Save": "Speichern",
+  "SaveAs": "Speichern unter",
   "SaveImage": "Bild speichern",
   "SavingFileFailed": "Speichern der Datei fehlgeschlagen",
   "ScrollAndRotate": "Scrollen und drehen",

+ 1 - 0
src/PicView.Core/Config/Languages/en.json

@@ -283,6 +283,7 @@
   "Rotated": "Rotated",
   "Saturation": "Saturation",
   "Save": "Save",
+  "SaveAs": "Save as",
   "SaveImage": "Save image",
   "SavingFileFailed": "Saving file failed",
   "ScrollAndRotate": "Scroll and rotate",

+ 1 - 0
src/PicView.Core/Config/Languages/es.json

@@ -282,6 +282,7 @@
   "Rotated": "Rotado",
   "Saturation": "Saturación",
   "Save": "Guardar",
+  "SaveAs": "Guardar como",
   "SaveImage": "Guardar Imagen",
   "SavingFileFailed": "Guardando archivo fallido",
   "ScrollAndRotate": "Desplazar y rotar",

+ 2 - 1
src/PicView.Core/Config/Languages/fr.json

@@ -282,7 +282,8 @@
   "RotateRight": "Pivoter à droite",
   "Rotated": "Tourné",
   "Saturation": "Saturation",
-  "Save": "Sauvegarder",
+  "Save": "Enregistrer",
+  "SaveAs": "Enregistrer sous",
   "SaveImage": "Enregistrer l'image",
   "SavingFileFailed": "La sauvegarde du fichier a échoué",
   "ScrollAndRotate": "Faire défiler et tourner",

+ 1 - 0
src/PicView.Core/Config/Languages/it.json

@@ -283,6 +283,7 @@
   "Rotated": "Ruotato",
   "Saturation": "Saturazione",
   "Save": "Salva",
+  "SaveAs": "Salva come",
   "SaveImage": "Salva immagine",
   "SavingFileFailed": "Salvataggio del file fallito",
   "ScrollAndRotate": "Scorrere e ruotare",

+ 1 - 0
src/PicView.Core/Config/Languages/ko.json

@@ -283,6 +283,7 @@
   "Rotated": "회전됨",
   "Saturation": "채도",
   "Save": "저장",
+  "SaveAs": "다른 이름으로 저장",
   "SaveImage": "이미지 저장",
   "SavingFileFailed": "파일 저장 실패",
   "ScrollAndRotate": "스크롤 및 회전",

+ 1 - 0
src/PicView.Core/Config/Languages/pl.json

@@ -283,6 +283,7 @@
   "Rotated": "Obrócone",
   "Saturation": "Nasycenie",
   "Save": "Zapisz",
+  "SaveAs": "Zapisz jako",
   "SaveImage": "Zapisz obraz",
   "SavingFileFailed": "Zapis pliku nie powiódł się",
   "ScrollAndRotate": "Przewijaj i obracaj",

+ 1 - 0
src/PicView.Core/Config/Languages/ro.json

@@ -283,6 +283,7 @@
   "Rotated": "Rotită",
   "Saturation": "Saturație",
   "Save": "Salvare",
+  "SaveAs": "Salvează ca",
   "SaveImage": "Salvare imagine",
   "SavingFileFailed": "Salvarea fișierului a eșuat",
   "ScrollAndRotate": "Derulați și rotiți",

+ 1 - 0
src/PicView.Core/Config/Languages/ru.json

@@ -283,6 +283,7 @@
   "Rotated": "Повернуто",
   "Saturation": "Насыщенность",
   "Save": "Сохранить",
+  "SaveAs": "Сохранить как",
   "SaveImage": "Сохранить изображение",
   "SavingFileFailed": "Ошибка сохранения файла",
   "ScrollAndRotate": "Прокрутка и вращение",

+ 1 - 0
src/PicView.Core/Config/Languages/zh-CN.json

@@ -283,6 +283,7 @@
   "Rotated": "已旋转",
   "Saturation": "饱和度",
   "Save": "保存",
+  "SaveAs": "另存为",
   "SaveImage": "保存图片",
   "SavingFileFailed": "文件保存失败",
   "ScrollAndRotate": "滚动和旋转",

+ 1 - 0
src/PicView.Core/Config/Languages/zh-TW.json

@@ -283,6 +283,7 @@
   "Rotated": "已旋轉",
   "Saturation": "飽和度",
   "Save": "儲存",
+  "SaveAs": "另存新檔",
   "SaveImage": "儲存圖片",
   "SavingFileFailed": "檔案儲存失敗",
   "ScrollAndRotate": "捲動和旋轉",

+ 51 - 9
src/PicView.Core/Extensions/StringExtensions.cs

@@ -1,25 +1,67 @@
-namespace PicView.Core.Extensions;
+using System.Text.RegularExpressions;
 
-public static class StringExtensions
+namespace PicView.Core.Extensions;
+
+/// <summary>
+/// Provides extension methods for the <see cref="string"/> class.
+/// </summary>
+public static partial class StringExtensions
 {
-    public static string FirstCharToUpper(this string input) =>
-        input switch
+    /// <summary>
+    /// Converts the first character of the string to uppercase.
+    /// </summary>
+    /// <param name="input">The input string.</param>
+    /// <returns>A string with the first character capitalized. If the string is null or empty, an empty string is returned.</returns>
+    public static string FirstCharToUpper(this string input)
+    {
+        return input switch
         {
             null => string.Empty,
             "" => string.Empty,
             _ => string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1))
         };
-    
+    }
+
     /// <summary>
-    /// Shortens the given string `name` to the given `amount` and appends "..." to it.
+    /// Shortens the given string <paramref name="name"/> to the specified <paramref name="amount"/> and appends "..." to it.
     /// </summary>
-    /// <param name="name">The string to shorten</param>
-    /// <param name="amount">The length to shorten the string to</param>
-    /// <returns>The shortened string</returns>
+    /// <param name="name">The string to shorten.</param>
+    /// <param name="amount">The length to shorten the string to.</param>
+    /// <returns>The shortened string with "..." appended at the end.</returns>
     public static string Shorten(this string name, int amount)
     {
         name = name[..amount];
         name += "...";
         return name;
     }
+
+    /// <summary>
+    /// Extracts the percentage value from the string, if present.
+    /// </summary>
+    /// <param name="text">The string containing a percentage value.</param>
+    /// <returns>The percentage value found in the string, or 0 if no valid percentage is found.</returns>
+    public static double GetPercentage(this string text)
+    {
+        foreach (Match match in PercentageRegex().Matches(text)) // Find % sign
+        {
+            if (!match.Success)
+            {
+                continue;
+            }
+
+            if (double.TryParse(match.Groups[1].Value, out var percentage))
+            {
+                return percentage;
+            }
+        }
+
+        return 0;
+    }
+
+    /// <summary>
+    /// A regex pattern used to match percentage values (e.g., "50%").
+    /// </summary>
+    /// <returns>A regex pattern that matches numbers followed by a percentage sign.</returns>
+    [GeneratedRegex("(\\d+)%")]
+    private static partial Regex PercentageRegex();
 }

+ 22 - 2
src/PicView.Core/ImageDecoding/SaveImageFileHelper.cs

@@ -44,11 +44,31 @@ public static class SaveImageFileHelper
 
             if (width is not null)
             {
-                magickImage.Resize(width.Value, 0);
+                if (height is not null)
+                {
+                    if (height >= 0)
+                    {
+                        magickImage.Resize(0, height.Value);
+                    }
+                }
+                else
+                {
+                    magickImage.Resize(width.Value, 0);
+                }
             }
             else if (height is not null)
             {
-                magickImage.Resize(0, height.Value);
+                if (width is not null)
+                {
+                    if (width >= 0)
+                    {
+                        magickImage.Resize(width.Value, 0);
+                    }
+                }
+                else
+                {
+                    magickImage.Resize(0, height.Value);
+                }
             }
 
             if (rotationAngle is not null)

+ 2 - 0
src/PicView.Core/Localization/LanguageModel.cs

@@ -307,6 +307,8 @@ public record LanguageModel
     public string? Lossless { get; set; }
     public string? Lossy { get; set; }
     public string? Quality { get; set; }
+    
+    public string? SaveAs { get; set; }
     public string? Percentage { get; set; }
     public string? GenerateThumbnails { get; set; }
     public string? Thumbnail { get; set; }

部分文件因为文件数量过多而无法显示