Browse Source

Update file history to include pinning file functionality. Updated UI styling, as well as scrollbar styling. Refactor/rename/restructure.

Ruben 6 tháng trước cách đây
mục cha
commit
2b9cc02413
58 tập tin đã thay đổi với 1181 bổ sung321 xóa
  1. 2 2
      src/PicView.Avalonia.Win32/Views/BatchResizeResizeWindow.axaml
  2. 4 3
      src/PicView.Avalonia.Win32/Views/KeybindingsWindow.axaml
  3. 0 7
      src/PicView.Avalonia/CustomControls/MainScrollViewer.cs
  4. 4 4
      src/PicView.Avalonia/Functions/FunctionsMapper.cs
  5. 2 2
      src/PicView.Avalonia/Navigation/ErrorHandling.cs
  6. 5 4
      src/PicView.Avalonia/Navigation/ImageIterator.cs
  7. 3 1
      src/PicView.Avalonia/Navigation/ImageLoader.cs
  8. 3 2
      src/PicView.Avalonia/Navigation/NavigationManager.cs
  9. 4 4
      src/PicView.Avalonia/PicView.Avalonia.csproj
  10. 0 1
      src/PicView.Avalonia/PicViewTheme/AllControls.axaml
  11. 87 0
      src/PicView.Avalonia/PicViewTheme/Controls/AutoScrollViewer.axaml
  12. 17 5
      src/PicView.Avalonia/PicViewTheme/Controls/Button.axaml
  13. 4 2
      src/PicView.Avalonia/PicViewTheme/Controls/ComboBox.axaml
  14. 3 2
      src/PicView.Avalonia/PicViewTheme/Controls/ListBox.axaml
  15. 0 52
      src/PicView.Avalonia/PicViewTheme/Controls/MainScrollViewer.axaml
  16. 4 4
      src/PicView.Avalonia/PicViewTheme/Controls/MainScrollbar.axaml
  17. 59 9
      src/PicView.Avalonia/PicViewTheme/Controls/Menu.axaml
  18. 79 5
      src/PicView.Avalonia/PicViewTheme/Controls/Scrollbar.axaml
  19. 44 0
      src/PicView.Avalonia/PicViewTheme/Icons.axaml
  20. 3 1
      src/PicView.Avalonia/PicViewTheme/ResourceDictionaries/MainColors.axaml
  21. 2 2
      src/PicView.Avalonia/StartUp/QuickLoad.cs
  22. 2 2
      src/PicView.Avalonia/StartUp/StartUpHelper.cs
  23. 96 0
      src/PicView.Avalonia/UI/FileHistory/FileHistoryMenuBuilder.cs
  24. 91 0
      src/PicView.Avalonia/UI/FileHistory/FileHistoryMenuController.cs
  25. 154 0
      src/PicView.Avalonia/UI/FileHistory/FileHistoryMenuItem.cs
  26. 2 2
      src/PicView.Avalonia/Views/AppearanceView.axaml
  27. 36 36
      src/PicView.Avalonia/Views/AppearanceView.axaml.cs
  28. 3 2
      src/PicView.Avalonia/Views/ImageViewer.axaml
  29. 1 0
      src/PicView.Avalonia/Views/LanguageView.axaml
  30. 72 1
      src/PicView.Avalonia/Views/MainView.axaml
  31. 34 74
      src/PicView.Avalonia/Views/MainView.axaml.cs
  32. 32 0
      src/PicView.Avalonia/Views/UC/Buttons/PinButton.axaml
  33. 19 0
      src/PicView.Avalonia/Views/UC/Buttons/PinButton.axaml.cs
  34. 4 4
      src/PicView.Avalonia/WindowBehavior/WindowFunctions.cs
  35. 5 0
      src/PicView.Core/Config/Languages/da.json
  36. 5 0
      src/PicView.Core/Config/Languages/de.json
  37. 5 0
      src/PicView.Core/Config/Languages/en.json
  38. 5 0
      src/PicView.Core/Config/Languages/es.json
  39. 5 0
      src/PicView.Core/Config/Languages/fr.json
  40. 5 0
      src/PicView.Core/Config/Languages/it.json
  41. 5 0
      src/PicView.Core/Config/Languages/ja.json
  42. 5 0
      src/PicView.Core/Config/Languages/ko.json
  43. 5 0
      src/PicView.Core/Config/Languages/nl.json
  44. 5 0
      src/PicView.Core/Config/Languages/pl.json
  45. 5 0
      src/PicView.Core/Config/Languages/pt-br.json
  46. 5 0
      src/PicView.Core/Config/Languages/ro.json
  47. 5 0
      src/PicView.Core/Config/Languages/ru.json
  48. 5 0
      src/PicView.Core/Config/Languages/sv.json
  49. 5 0
      src/PicView.Core/Config/Languages/tr.json
  50. 5 0
      src/PicView.Core/Config/Languages/zh-CN.json
  51. 5 0
      src/PicView.Core/Config/Languages/zh-TW.json
  52. 15 0
      src/PicView.Core/Config/SettingsConfiguration.cs
  53. 19 16
      src/PicView.Core/Config/SettingsManager.cs
  54. 15 0
      src/PicView.Core/FileHistory/FileHistoryEntries.cs
  55. 131 71
      src/PicView.Core/FileHistory/FileHistoryManager.cs
  56. 5 0
      src/PicView.Core/Localization/LanguageModel.cs
  57. 1 1
      src/PicView.Core/PicView.Core.csproj
  58. 35 0
      src/PicView.Core/ViewModels/TranslationViewModel.cs

+ 2 - 2
src/PicView.Avalonia.Win32/Views/BatchResizeResizeWindow.axaml

@@ -83,13 +83,13 @@
                 Height="1"
                 x:Name="BorderRectangle" />
 
-            <customControls:MainScrollViewer x:Name="ScrollViewer">
+            <customControls:AutoScrollViewer Theme="{StaticResource Main}" x:Name="ScrollViewer">
                 <views:BatchResizeView
                     Background="{DynamicResource NoisyTexture}"
                     Focusable="True"
                     Margin="0"
                     Padding="10,2,5,10" />
-            </customControls:MainScrollViewer>
+            </customControls:AutoScrollViewer>
         </StackPanel>
     </Border>
 </Window>

+ 4 - 3
src/PicView.Avalonia.Win32/Views/KeybindingsWindow.axaml

@@ -84,13 +84,14 @@
                 VerticalAlignment="Top"
                 x:Name="BorderRectangle" />
 
-            <customControls:MainScrollViewer
+            <customControls:AutoScrollViewer
                 Background="{DynamicResource NoisyTexture}"
                 Focusable="True"
                 Margin="0,28,0,0"
-                Padding="5,2,5,10">
+                Padding="5,2,5,10"
+                Theme="{StaticResource Main}">
                 <views:KeybindingsView Background="Transparent" PointerPressed="MoveWindow" />
-            </customControls:MainScrollViewer>
+            </customControls:AutoScrollViewer>
         </Panel>
     </Border>
 </Window>

+ 0 - 7
src/PicView.Avalonia/CustomControls/MainScrollViewer.cs

@@ -1,7 +0,0 @@
-namespace PicView.Avalonia.CustomControls;
-
-
-public class MainScrollViewer : AutoScrollViewer
-{
-    protected override Type StyleKeyOverride => typeof(MainScrollViewer);
-}

+ 4 - 4
src/PicView.Avalonia/Functions/FunctionsMapper.cs

@@ -14,9 +14,9 @@ using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.FileHandling;
+using PicView.Core.FileHistory;
 using PicView.Core.ImageDecoding;
 using PicView.Core.Keybindings;
-using PicView.Core.Navigation;
 using PicView.Core.ProcessHandling;
 
 namespace PicView.Avalonia.Functions;
@@ -507,15 +507,15 @@ public static class FunctionsMapper
 
     /// <inheritdoc cref="NavigationManager.LoadPicFromStringAsync(string, MainViewModel)" />
     public static async Task OpenLastFile() =>
-        await NavigationManager.LoadPicFromStringAsync(FileHistory.GetLastEntry(), Vm).ConfigureAwait(false);
+        await NavigationManager.LoadPicFromStringAsync(FileHistoryManager.GetLastEntry(), Vm).ConfigureAwait(false);
 
     /// <inheritdoc cref="NavigationManager.LoadPicFromStringAsync(string, MainViewModel)" />
     public static async Task OpenPreviousFileHistoryEntry() =>
-        await NavigationManager.LoadPicFromStringAsync(FileHistory.GetPreviousEntry(), Vm).ConfigureAwait(false);
+        await NavigationManager.LoadPicFromStringAsync(FileHistoryManager.GetPreviousEntry(), Vm).ConfigureAwait(false);
    
     /// <inheritdoc cref="NavigationManager.LoadPicFromStringAsync(string, MainViewModel)" />
     public static async Task OpenNextFileHistoryEntry() =>
-        await NavigationManager.LoadPicFromStringAsync(FileHistory.GetNextEntry(), Vm).ConfigureAwait(false);
+        await NavigationManager.LoadPicFromStringAsync(FileHistoryManager.GetNextEntry(), Vm).ConfigureAwait(false);
     
     /// <inheritdoc cref="FileManager.Print(string, MainViewModel)" />
     public static async Task Print() =>

+ 2 - 2
src/PicView.Avalonia/Navigation/ErrorHandling.cs

@@ -5,8 +5,8 @@ using PicView.Avalonia.Gallery;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Core.FileHandling;
+using PicView.Core.FileHistory;
 using PicView.Core.Gallery;
-using PicView.Core.Navigation;
 using PicView.Core.Sizing;
 using StartUpMenu = PicView.Avalonia.Views.StartUpMenu;
 
@@ -79,7 +79,7 @@ public static class ErrorHandling
         
         if (!NavigationManager.CanNavigate(vm))
         {
-            await NavigationManager.LoadPicFromStringAsync(FileHistory.GetLastEntry(), vm).ConfigureAwait(false);
+            await NavigationManager.LoadPicFromStringAsync(FileHistoryManager.GetLastEntry(), vm).ConfigureAwait(false);
             return;
         }
         

+ 5 - 4
src/PicView.Avalonia/Navigation/ImageIterator.cs

@@ -7,6 +7,7 @@ using PicView.Avalonia.Preloading;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Core.FileHandling;
+using PicView.Core.FileHistory;
 using PicView.Core.Gallery;
 using PicView.Core.Navigation;
 using Timer = System.Timers.Timer;
@@ -274,7 +275,7 @@ public class ImageIterator : IAsyncDisposable
                 PreLoader.Resynchronize(ImagePaths);
             }
             
-            FileHistory.Remove(e.FullPath);
+            FileHistoryManager.Remove(e.FullPath);
 
         }
         catch (Exception exception)
@@ -355,7 +356,7 @@ public class ImageIterator : IAsyncDisposable
             Resynchronize();
 
             isRunning = false;
-            FileHistory.Rename(e.OldFullPath, e.FullPath);
+            FileHistoryManager.Rename(e.OldFullPath, e.FullPath);
             await Dispatcher.UIThread.InvokeAsync(() =>
                 GalleryFunctions.RenameGalleryItem(oldIndex, index, Path.GetFileNameWithoutExtension(e.Name), e.FullPath,
                     _vm));
@@ -711,10 +712,10 @@ public class ImageIterator : IAsyncDisposable
             // Add recent files
             if (string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath) && ImagePaths.Count > CurrentIndex)
             {
-                FileHistory.Add(ImagePaths[CurrentIndex]);
+                FileHistoryManager.Add(ImagePaths[CurrentIndex]);
                 if (Settings.ImageScaling.ShowImageSideBySide)
                 {
-                    FileHistory.Add(ImagePaths[GetIteration(CurrentIndex, IsReversed ? NavigateTo.Previous : NavigateTo.Next)]);
+                    FileHistoryManager.Add(ImagePaths[GetIteration(CurrentIndex, IsReversed ? NavigateTo.Previous : NavigateTo.Next)]);
                 }
             }
         }

+ 3 - 1
src/PicView.Avalonia/Navigation/ImageLoader.cs

@@ -5,6 +5,7 @@ using PicView.Avalonia.Input;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Core.ArchiveHandling;
+using PicView.Core.FileHistory;
 using PicView.Core.Gallery;
 using PicView.Core.Http;
 using PicView.Core.ImageDecoding;
@@ -245,6 +246,7 @@ public static class ImageLoader
                 await LoadPicFromDirectoryAsync(ArchiveExtraction.TempZipDirectory, vm).ConfigureAwait(false);
             }
 
+            FileHistoryManager.Add(path);
             MainKeyboardShortcuts.ClearKeyDownModifiers(); // Fix possible modifier key state issue
         }
         else
@@ -332,7 +334,7 @@ public static class ImageLoader
         vm.IsLoading = false;
         vm.PicViewer.FileInfo = fileInfo;
         vm.PicViewer.ExifOrientation = imageModel.EXIFOrientation;
-        FileHistory.Add(url);
+        FileHistoryManager.Add(url);
 
         await NavigationManager.DisposeImageIteratorAsync();
     }

+ 3 - 2
src/PicView.Avalonia/Navigation/NavigationManager.cs

@@ -6,6 +6,7 @@ using PicView.Avalonia.Preloading;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.WindowBehavior;
+using PicView.Core.FileHistory;
 using PicView.Core.Gallery;
 using PicView.Core.ImageDecoding;
 using PicView.Core.Localization;
@@ -546,10 +547,10 @@ public static class NavigationManager
         }
 
         vm.IsLoading = false;
-        FileHistory.Add(_imageIterator.ImagePaths[index]);
+        FileHistoryManager.Add(_imageIterator.ImagePaths[index]);
         if (Settings.ImageScaling.ShowImageSideBySide)
         {
-            FileHistory.Add(_imageIterator.ImagePaths[_imageIterator.GetIteration(index, NavigateTo.Next)]);
+            FileHistoryManager.Add(_imageIterator.ImagePaths[_imageIterator.GetIteration(index, NavigateTo.Next)]);
         }
         await GalleryLoad.CheckAndReloadGallery(fileInfo, vm);
     }

+ 4 - 4
src/PicView.Avalonia/PicView.Avalonia.csproj

@@ -38,10 +38,6 @@
 
   
   <ItemGroup>
-    <AvaloniaXaml Remove="Themes\**" />
-    <Compile Remove="Themes\**" />
-    <EmbeddedResource Remove="Themes\**" />
-    <None Remove="Themes\**" />
     <Compile Update="Views\UC\DragDrogView.axaml.cs">
       <DependentUpon>DragDrogView.axaml</DependentUpon>
       <SubType>Code</SubType>
@@ -90,6 +86,10 @@
       <DependentUpon>EffectsView.axaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
+    <Compile Update="Views\UC\Buttons\PinButton.axaml.cs">
+      <DependentUpon>PinButton.axaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
   </ItemGroup>
 
   

+ 0 - 1
src/PicView.Avalonia/PicViewTheme/AllControls.axaml

@@ -22,7 +22,6 @@
                 <ResourceInclude Source="Controls/ListBox.axaml" />
                 <ResourceInclude Source="Controls/ListBoxItem.axaml" />
                 <ResourceInclude Source="Controls/MainScrollbar.axaml" />
-                <ResourceInclude Source="Controls/MainScrollViewer.axaml" />
                 <ResourceInclude Source="Controls/Menu.axaml" />
                 <ResourceInclude Source="Controls/MenuFlyoutPresenter.axaml" />
                 <ResourceInclude Source="Controls/NumericUpDown.axaml" />

+ 87 - 0
src/PicView.Avalonia/PicViewTheme/Controls/AutoScrollViewer.axaml

@@ -47,4 +47,91 @@
 
     </ControlTheme>
 
+    <ControlTheme TargetType="customControls:AutoScrollViewer" x:Key="Main">
+        <Setter Property="Background" Value="Transparent" />
+        <Setter Property="Template">
+            <ControlTemplate>
+                <Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
+                    <ScrollContentPresenter
+                        Background="{TemplateBinding Background}"
+                        HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
+                        HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
+                        Name="PART_ContentPresenter"
+                        Padding="{TemplateBinding Padding}"
+                        ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"
+                        VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
+                        VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}">
+                        <ScrollContentPresenter.GestureRecognizers>
+                            <ScrollGestureRecognizer
+                                CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
+                                CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
+                                IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}" />
+                        </ScrollContentPresenter.GestureRecognizers>
+                    </ScrollContentPresenter>
+                    <ScrollBar
+                        Grid.Column="0"
+                        Grid.Row="1"
+                        Name="PART_HorizontalScrollBar"
+                        Orientation="Horizontal"
+                        Theme="{StaticResource MainScrollbar}" />
+                    <ScrollBar
+                        Grid.Column="1"
+                        Grid.Row="0"
+                        Name="PART_VerticalScrollBar"
+                        Orientation="Vertical"
+                        Theme="{StaticResource MainScrollbar}" />
+                    <Canvas Grid.Column="0" Grid.Row="0">
+                        <customControls:AutoScrollSign
+                            HorizontalAlignment="Center"
+                            IsVisible="False"
+                            Name="PART_AutoScrollSign"
+                            VerticalAlignment="Center" />
+                    </Canvas>
+                </Grid>
+            </ControlTemplate>
+        </Setter>
+    </ControlTheme>
+
+    <ControlTheme TargetType="customControls:AutoScrollViewer" x:Key="Inline">
+        <Setter Property="Background" Value="Transparent" />
+        <Setter Property="Template">
+            <ControlTemplate>
+                <Panel>
+                    <ScrollContentPresenter
+                        Background="Transparent"
+                        HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
+                        HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
+                        Name="PART_ContentPresenter"
+                        Padding="{TemplateBinding Padding}"
+                        ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"
+                        VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
+                        VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}">
+                        <ScrollContentPresenter.GestureRecognizers>
+                            <ScrollGestureRecognizer
+                                CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
+                                CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
+                                IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}" />
+                        </ScrollContentPresenter.GestureRecognizers>
+                    </ScrollContentPresenter>
+                    <ScrollBar
+                        Name="PART_HorizontalScrollBar"
+                        Orientation="Horizontal"
+                        Theme="{StaticResource Floating}" />
+                    <ScrollBar
+                        HorizontalAlignment="Right"
+                        Name="PART_VerticalScrollBar"
+                        Orientation="Vertical"
+                        Theme="{StaticResource Floating}" />
+                    <Canvas>
+                        <customControls:AutoScrollSign
+                            HorizontalAlignment="Center"
+                            IsVisible="False"
+                            Name="PART_AutoScrollSign"
+                            VerticalAlignment="Center" />
+                    </Canvas>
+                </Panel>
+            </ControlTemplate>
+        </Setter>
+    </ControlTheme>
+
 </ResourceDictionary>

+ 17 - 5
src/PicView.Avalonia/PicViewTheme/Controls/Button.axaml

@@ -3,6 +3,8 @@
     <ControlTheme TargetType="Button" x:Key="{x:Type Button}">
         <Setter Property="ClickMode" Value="Press" />
         <Setter Property="Focusable" Value="False" />
+        <Setter Property="ToolTip.BetweenShowDelay" Value="-1" />
+        <Setter Property="ToolTip.ShowDelay" Value="900" />
 
         <Setter Property="Button.Template">
             <ControlTemplate TargetType="Button">
@@ -93,7 +95,7 @@
                 <Animation Duration="{StaticResource ButtonHoverAnimationDuration}" IterationCount="1">
                     <KeyFrame>
                         <Setter Property="Background">
-                            <SolidColorBrush Color="#992420" />
+                            <SolidColorBrush Color="{StaticResource ErrorColor}" />
                         </Setter>
                     </KeyFrame>
                     <KeyFrame Cue="1">
@@ -101,13 +103,13 @@
                             <SolidColorBrush Color="{StaticResource SecondaryTextColor}" />
                         </Setter>
                         <Setter Property="Background">
-                            <SolidColorBrush Color="#992420" />
+                            <SolidColorBrush Color="{StaticResource ErrorColor}" />
                         </Setter>
                     </KeyFrame>
                 </Animation>
             </Style.Animations>
             <Setter Property="Foreground" Value="{StaticResource SecondaryTextColor}" />
-            <Setter Property="Background" Value="#992420" />
+            <Setter Property="Background" Value="{StaticResource ErrorColor}" />
         </Style>
         <Style Selector="^.altHover:pointerover /template/ ContentPresenter#PART_ContentPresenter">
             <Style.Animations>
@@ -170,10 +172,20 @@
 
         <!--  End of animations  -->
 
-        <Style Selector="^.hover:pressed  /template/ ContentPresenter#PART_ContentPresenter">
+        <Style Selector="^.noAnimHover:pointerover /template/ ContentPresenter#PART_ContentPresenter">
+            <Setter Property="Background" Value="{DynamicResource AccentColor}" />
+            <Setter Property="Foreground" Value="{DynamicResource MainTextColor}" />
+        </Style>
+
+        <Style Selector="^.active /template/ ContentPresenter#PART_ContentPresenter">
+            <Setter Property="Background" Value="{DynamicResource SecondaryAccentColor}" />
+            <Setter Property="BorderBrush" Value="{DynamicResource SecondaryAccentColor}" />
+        </Style>
+
+        <Style Selector="^:not(.active).hover:pressed  /template/ ContentPresenter#PART_ContentPresenter">
             <Setter Property="Background" Value="{DynamicResource SecondaryAccentColor}" />
         </Style>
-        <Style Selector="^.altHover:pressed  /template/ ContentPresenter#PART_ContentPresenter">
+        <Style Selector="^:not(.active).altHover:pressed  /template/ ContentPresenter#PART_ContentPresenter">
             <Setter Property="Background" Value="{DynamicResource AltBackgroundHoverColor}" />
             <Setter Property="BorderBrush" Value="{DynamicResource SecondaryBorderColor}" />
         </Style>

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

@@ -1,6 +1,7 @@
 <ResourceDictionary
     x:ClassModifier="internal"
     xmlns="https://github.com/avaloniaui"
+    xmlns:customControls="clr-namespace:PicView.Avalonia.CustomControls"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
     <ControlTheme TargetType="ComboBox" x:Key="{x:Type ComboBox}">
         <Setter Property="Foreground" Value="{StaticResource SecondaryTextColor}" />
@@ -83,15 +84,16 @@
                                 Background="{DynamicResource DropDownBgColor}"
                                 BorderBrush="{DynamicResource SecondaryBorderColor}"
                                 BorderThickness="1,0,1,1">
-                                <ScrollViewer
+                                <customControls:AutoScrollViewer
                                     HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
                                     IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
+                                    Theme="{StaticResource Main}"
                                     VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
                                     <ItemsPresenter
                                         ItemsPanel="{TemplateBinding ItemsPanel}"
                                         Margin="4"
                                         Name="PART_ItemsPresenter" />
-                                </ScrollViewer>
+                                </customControls:AutoScrollViewer>
                             </Border>
                         </Popup>
                     </Panel>

+ 3 - 2
src/PicView.Avalonia/PicViewTheme/Controls/ListBox.axaml

@@ -15,7 +15,7 @@
                     BorderThickness="{TemplateBinding BorderThickness}"
                     CornerRadius="{TemplateBinding CornerRadius}"
                     Name="border">
-                    <customControls:MainScrollViewer
+                    <customControls:AutoScrollViewer
                         AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
                         Background="{TemplateBinding Background}"
                         BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}"
@@ -24,13 +24,14 @@
                         IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
                         IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
                         Name="PART_ScrollViewer"
+                        Theme="{StaticResource Main}"
                         VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
                         VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}">
                         <ItemsPresenter
                             ItemsPanel="{TemplateBinding ItemsPanel}"
                             Margin="{TemplateBinding Padding}"
                             Name="PART_ItemsPresenter" />
-                    </customControls:MainScrollViewer>
+                    </customControls:AutoScrollViewer>
                 </Border>
             </ControlTemplate>
         </Setter>

+ 0 - 52
src/PicView.Avalonia/PicViewTheme/Controls/MainScrollViewer.axaml

@@ -1,52 +0,0 @@
-<ResourceDictionary
-    x:ClassModifier="internal"
-    xmlns="https://github.com/avaloniaui"
-    xmlns:customControls="clr-namespace:PicView.Avalonia.CustomControls"
-    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    <ControlTheme TargetType="customControls:MainScrollViewer" x:Key="{x:Type customControls:MainScrollViewer}">
-        <Setter Property="Background" Value="Transparent" />
-        <Setter Property="Template">
-            <ControlTemplate>
-                <Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
-                    <ScrollContentPresenter
-                        Background="{TemplateBinding Background}"
-                        HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}"
-                        HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}"
-                        Name="PART_ContentPresenter"
-                        Padding="{TemplateBinding Padding}"
-                        ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"
-                        VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}"
-                        VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}">
-                        <ScrollContentPresenter.GestureRecognizers>
-                            <ScrollGestureRecognizer
-                                CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
-                                CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
-                                IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}" />
-                        </ScrollContentPresenter.GestureRecognizers>
-                    </ScrollContentPresenter>
-                    <ScrollBar
-                        Grid.Column="0"
-                        Grid.Row="1"
-                        Name="PART_HorizontalScrollBar"
-                        Orientation="Horizontal"
-                        Theme="{StaticResource MainScrollbar}" />
-                    <ScrollBar
-                        Grid.Column="1"
-                        Grid.Row="0"
-                        Name="PART_VerticalScrollBar"
-                        Orientation="Vertical"
-                        Theme="{StaticResource MainScrollbar}" />
-                    <Canvas Grid.Column="0" Grid.Row="0">
-                        <customControls:AutoScrollSign
-                            HorizontalAlignment="Center"
-                            IsVisible="False"
-                            Name="PART_AutoScrollSign"
-                            VerticalAlignment="Center" />
-                    </Canvas>
-                </Grid>
-            </ControlTemplate>
-        </Setter>
-
-    </ControlTheme>
-
-</ResourceDictionary>

+ 4 - 4
src/PicView.Avalonia/PicViewTheme/Controls/MainScrollbar.axaml

@@ -28,7 +28,7 @@
                                 VerticalAlignment="Center"
                                 Width="16">
                                 <Path
-                                    Data="M321.94 98L158.82 237.78a24 24 0 000 36.44L321.94 414c15.57 13.34 39.62 2.28 39.62-18.22v-279.6c0-20.5-24.05-31.56-39.62-18.18z"
+                                    Data="{StaticResource FilledArrowLeftGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="12"
                                     HorizontalAlignment="Center"
@@ -76,7 +76,7 @@
                                 VerticalAlignment="Center"
                                 Width="16">
                                 <Path
-                                    Data="M190.06 414l163.12-139.78a24 24 0 000-36.44L190.06 98c-15.57-13.34-39.62-2.28-39.62 18.22v279.6c0 20.5 24.05 31.56 39.62 18.18z"
+                                    Data="{StaticResource FilledArrowRightGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="12"
                                     HorizontalAlignment="Center"
@@ -110,7 +110,7 @@
                                 MinHeight="{StaticResource ScrollBarThickness}"
                                 Name="PART_LineUpButton">
                                 <Path
-                                    Data="F1 M512,512z M0,0z M414,321.94L274.22,158.82A24,24,0,0,0,237.78,158.82L98,321.94C84.66,337.51,95.72,361.56,116.22,361.56L395.82,361.56C416.32,361.56,427.38,337.51,414,321.94z"
+                                    Data="{StaticResource FilledArrowUpGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="16"
                                     HorizontalAlignment="Center"
@@ -158,7 +158,7 @@
                                 MinHeight="{StaticResource ScrollBarThickness}"
                                 Name="PART_LineDownButton">
                                 <Path
-                                    Data="F1 M512,512z M0,0z M98,190.06L237.78,353.18A24,24,0,0,0,274.22,353.18L414,190.06C427.34,174.49,416.28,150.44,395.78,150.44L116.18,150.44C95.6799999999999,150.44,84.6199999999999,174.49,97.9999999999999,190.06z"
+                                    Data="{StaticResource FilledArrowDownGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="16"
                                     HorizontalAlignment="Center"

+ 59 - 9
src/PicView.Avalonia/PicViewTheme/Controls/Menu.axaml

@@ -95,13 +95,11 @@
                                 BorderBrush="{DynamicResource MainBorderColor}"
                                 BorderThickness="1"
                                 CornerRadius="8">
-                                <ScrollViewer>
-                                    <ItemsPresenter
-                                        Grid.IsSharedSizeScope="True"
-                                        ItemsPanel="{TemplateBinding ItemsPanel}"
-                                        Margin="2"
-                                        Name="PART_ItemsPresenter" />
-                                </ScrollViewer>
+                                <ItemsPresenter
+                                    Grid.IsSharedSizeScope="True"
+                                    ItemsPanel="{TemplateBinding ItemsPanel}"
+                                    Margin="2"
+                                    Name="PART_ItemsPresenter" />
                             </Border>
                         </Popup>
                     </Grid>
@@ -144,12 +142,12 @@
             </Setter>
         </Style>
 
-        <Style Selector="^:selected /template/ Border#root">
+        <Style Selector="^:not(.noHover):selected /template/ Border#root">
             <Setter Property="Background" Value="{DynamicResource AccentColor}" />
             <Setter Property="BorderBrush" Value="{DynamicResource AccentColor}" />
         </Style>
 
-        <Style Selector="^:selected /template/ ContentPresenter#PART_HeaderPresenter">
+        <Style Selector="^:not(.noHover):selected /template/ ContentPresenter#PART_HeaderPresenter">
             <Setter Property="Foreground" Value="{StaticResource SecondaryTextColor}" />
             <Setter Property="Effect" Value="{StaticResource TextShadow}" />
         </Style>
@@ -191,6 +189,58 @@
 
     </ControlTheme>
 
+    <ControlTheme TargetType="MenuItem" x:Key="Slim">
+        <Setter Property="Background" Value="Transparent" />
+        <Setter Property="BorderThickness" Value="1" />
+        <Setter Property="Template">
+            <ControlTemplate>
+                <Border Name="root">
+                    <Panel>
+                        <ContentPresenter
+                            Content="{TemplateBinding Header}"
+                            ContentTemplate="{TemplateBinding HeaderTemplate}"
+                            Foreground="{DynamicResource ContextMenuTextColor}"
+                            Margin="{TemplateBinding Padding}"
+                            Name="PART_HeaderPresenter"
+                            VerticalAlignment="Center">
+                            <ContentPresenter.DataTemplates>
+                                <DataTemplate DataType="sys:String">
+                                    <AccessText Text="{Binding}" />
+                                </DataTemplate>
+                            </ContentPresenter.DataTemplates>
+                        </ContentPresenter>
+                        <Popup
+                            IsLightDismissEnabled="False"
+                            IsOpen="{TemplateBinding IsSubMenuOpen,
+                                                     Mode=TwoWay}"
+                            Name="PART_Popup"
+                            Placement="RightEdgeAlignedTop">
+                            <Border
+                                Background="{DynamicResource ContextMenuBackgroundColor}"
+                                BorderBrush="{DynamicResource MainBorderColor}"
+                                BorderThickness="1"
+                                CornerRadius="8">
+                                <ItemsPresenter
+                                    Grid.IsSharedSizeScope="True"
+                                    ItemsPanel="{TemplateBinding ItemsPanel}"
+                                    Margin="2"
+                                    Name="PART_ItemsPresenter" />
+                            </Border>
+                        </Popup>
+                    </Panel>
+                </Border>
+            </ControlTemplate>
+        </Setter>
+
+        <Style Selector="^:separator">
+            <Setter Property="Template">
+                <ControlTemplate>
+                    <Separator />
+                </ControlTemplate>
+            </Setter>
+        </Style>
+    </ControlTheme>
+
     <ControlTheme TargetType="Separator" x:Key="{x:Type Separator}">
         <Setter Property="Focusable" Value="False" />
         <Setter Property="Background" Value="{DynamicResource MainBorderColor}" />

+ 79 - 5
src/PicView.Avalonia/PicViewTheme/Controls/Scrollbar.axaml

@@ -25,7 +25,7 @@
                                 VerticalAlignment="Center"
                                 Width="16">
                                 <Path
-                                    Data="M321.94 98L158.82 237.78a24 24 0 000 36.44L321.94 414c15.57 13.34 39.62 2.28 39.62-18.22v-279.6c0-20.5-24.05-31.56-39.62-18.18z"
+                                    Data="{StaticResource FilledArrowLeftGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="12"
                                     HorizontalAlignment="Center"
@@ -73,7 +73,7 @@
                                 VerticalAlignment="Center"
                                 Width="16">
                                 <Path
-                                    Data="M190.06 414l163.12-139.78a24 24 0 000-36.44L190.06 98c-15.57-13.34-39.62-2.28-39.62 18.22v279.6c0 20.5 24.05 31.56 39.62 18.18z"
+                                    Data="{StaticResource FilledArrowRightGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="12"
                                     HorizontalAlignment="Center"
@@ -93,7 +93,6 @@
                 <ControlTemplate>
                     <Border
                         Background="Transparent"
-                        BorderThickness="1,0,0,0"
                         UseLayoutRounding="False"
                         x:Name="Border">
                         <Grid RowDefinitions="Auto,*,Auto">
@@ -107,7 +106,7 @@
                                 MinHeight="{StaticResource ScrollBarThickness}"
                                 Name="PART_LineUpButton">
                                 <Path
-                                    Data="F1 M512,512z M0,0z M414,321.94L274.22,158.82A24,24,0,0,0,237.78,158.82L98,321.94C84.66,337.51,95.72,361.56,116.22,361.56L395.82,361.56C416.32,361.56,427.38,337.51,414,321.94z"
+                                    Data="{StaticResource FilledArrowUpGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="16"
                                     HorizontalAlignment="Center"
@@ -155,7 +154,7 @@
                                 MinHeight="{StaticResource ScrollBarThickness}"
                                 Name="PART_LineDownButton">
                                 <Path
-                                    Data="F1 M512,512z M0,0z M98,190.06L237.78,353.18A24,24,0,0,0,274.22,353.18L414,190.06C427.34,174.49,416.28,150.44,395.78,150.44L116.18,150.44C95.6799999999999,150.44,84.6199999999999,174.49,97.9999999999999,190.06z"
+                                    Data="{StaticResource FilledArrowDownGeometry}"
                                     Fill="{TemplateBinding Foreground}"
                                     Height="16"
                                     HorizontalAlignment="Center"
@@ -168,6 +167,7 @@
                 </ControlTemplate>
             </Setter>
         </Style>
+
         <Style Selector="^ /template/ Thumb#thumb">
             <Setter Property="Background" Value="{DynamicResource MainBackgroundColor}" />
             <Setter Property="BorderThickness" Value="1" />
@@ -182,6 +182,7 @@
                 </Setter.Value>
             </Setter>
         </Style>
+
         <Style Selector="^ /template/ Thumb#thumb:pointerover">
             <Setter Property="Background" Value="{DynamicResource AccentColor}" />
         </Style>
@@ -217,4 +218,77 @@
         </Style>
 
     </ControlTheme>
+
+
+
+    <ControlTheme TargetType="ScrollBar" x:Key="Floating">
+        <Setter Property="Cursor" Value="Arrow" />
+
+        <Style Selector="^:horizontal">
+            <Setter Property="Height" Value="18" />
+            <Setter Property="Focusable" Value="False" />
+            <Setter Property="Template">
+                <ControlTemplate>
+                    <Border Background="Transparent" UseLayoutRounding="False">
+                        <Track
+                            DeferThumbDrag="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
+                            Maximum="{TemplateBinding Maximum}"
+                            Minimum="{TemplateBinding Minimum}"
+                            Orientation="{TemplateBinding Orientation}"
+                            Value="{TemplateBinding Value,
+                                                    Mode=TwoWay}"
+                            ViewportSize="{TemplateBinding ViewportSize}" />
+                    </Border>
+                </ControlTemplate>
+            </Setter>
+        </Style>
+
+        <Style Selector="^:vertical">
+            <Setter Property="Template">
+                <ControlTemplate>
+                    <Track
+                        DeferThumbDrag="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
+                        IsDirectionReversed="True"
+                        Maximum="{TemplateBinding Maximum}"
+                        Minimum="{TemplateBinding Minimum}"
+                        Orientation="{TemplateBinding Orientation}"
+                        Value="{TemplateBinding Value,
+                                                Mode=TwoWay}"
+                        ViewportSize="{TemplateBinding ViewportSize}">
+                        <Thumb Name="thumb" />
+                    </Track>
+                </ControlTemplate>
+            </Setter>
+        </Style>
+        <Style Selector="^ /template/ Thumb#thumb">
+            <Setter Property="Background" Value="{DynamicResource MainBackgroundColor}" />
+            <Setter Property="BorderBrush" Value="{DynamicResource TertiaryBorderColor}" />
+            <Setter Property="BorderThickness" Value="1" />
+            <Setter Property="Template">
+                <Setter.Value>
+                    <ControlTemplate>
+                        <Border
+                            Background="{TemplateBinding Background}"
+                            BorderBrush="{TemplateBinding BorderBrush}"
+                            BorderThickness="{TemplateBinding BorderThickness}" />
+                    </ControlTemplate>
+                </Setter.Value>
+            </Setter>
+        </Style>
+        <Style Selector="^ /template/ Thumb#thumb:pointerover">
+            <Setter Property="Background" Value="{DynamicResource AccentColor}" />
+        </Style>
+        <Style Selector="^ /template/ Thumb#thumb:pressed">
+            <Setter Property="Background" Value="{DynamicResource SecondaryAccentColor}" />
+        </Style>
+        <Style Selector="^:horizontal /template/ Thumb#thumb">
+            <Setter Property="MinWidth" Value="{StaticResource ScrollBarThickness}" />
+            <Setter Property="Height" Value="16" />
+        </Style>
+        <Style Selector="^:vertical /template/ Thumb#thumb">
+            <Setter Property="MinHeight" Value="{DynamicResource ScrollBarThickness}" />
+            <Setter Property="Width" Value="12" />
+        </Style>
+
+    </ControlTheme>
 </ResourceDictionary>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 44 - 0
src/PicView.Avalonia/PicViewTheme/Icons.axaml


+ 3 - 1
src/PicView.Avalonia/PicViewTheme/ResourceDictionaries/MainColors.axaml

@@ -47,6 +47,8 @@
 
             <Color x:Key="DropDownBgColor">#303030</Color>
 
+            <Color x:Key="DarkenBgColor">#14000000</Color>
+
 
             <ImageBrush
                 DestinationRect="0,0,200,200"
@@ -69,7 +71,7 @@
                 Direction="360"
                 x:Key="TextShadow" />
 
-            <Color x:Key="ErrorColor">#FFFF0000</Color>
+            <Color x:Key="ErrorColor">#B51522</Color>
             <SolidColorBrush Color="{StaticResource ErrorColor}" x:Key="ErrorBrush" />
         </ResourceDictionary>
 

+ 2 - 2
src/PicView.Avalonia/StartUp/QuickLoad.cs

@@ -6,9 +6,9 @@ using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.FileHandling;
+using PicView.Core.FileHistory;
 using PicView.Core.Gallery;
 using PicView.Core.ImageDecoding;
-using PicView.Core.Navigation;
 
 namespace PicView.Avalonia.StartUp;
 
@@ -170,7 +170,7 @@ public static class QuickLoad
         // Add recent files, except when browsing archive
         if (string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath))
         {
-            FileHistory.Add(fileInfo.FullName);
+            FileHistoryManager.Add(fileInfo.FullName);
         }
 
         NavigationManager.AddToPreloader(NavigationManager.GetCurrentIndex, imageModel);

+ 2 - 2
src/PicView.Avalonia/StartUp/StartUpHelper.cs

@@ -15,8 +15,8 @@ using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.Views;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.FileAssociations;
+using PicView.Core.FileHistory;
 using PicView.Core.Gallery;
-using PicView.Core.Navigation;
 using PicView.Core.ProcessHandling;
 using PicView.Core.Sizing;
 using PicView.Core.ViewModels;
@@ -147,7 +147,7 @@ public static class StartUpHelper
         }
         
         MenuManager.AddMenus();
-        FileHistory.Initialize();
+        FileHistoryManager.Initialize();
 
         Application.Current.Name = "PicView";
         

+ 96 - 0
src/PicView.Avalonia/UI/FileHistory/FileHistoryMenuBuilder.cs

@@ -0,0 +1,96 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.ViewModels;
+using PicView.Core.FileHistory;
+using PicView.Core.Localization;
+
+namespace PicView.Avalonia.UI.FileHistory;
+
+/// <summary>
+///     Responsible for building the file history menu UI elements
+/// </summary>
+public class FileHistoryMenuBuilder(Panel menuContainer, MainViewModel viewModel, bool isDescendingSorting)
+{
+    private readonly string? _currentFilePath = NavigationManager.GetCurrentFileName;
+
+    /// <summary>
+    ///     Builds the entire history menu with pinned and unpinned sections
+    /// </summary>
+    public void BuildMenu()
+    {
+        menuContainer.Children.Clear();
+
+        var entries = FileHistoryManager.AllEntries;
+
+        var pinnedEntries = entries.Where(e => e.IsPinned).ToList();
+        var unpinnedEntries = entries.Where(e => !e.IsPinned).ToList();
+
+        // Add pinned entries section
+        BuildPinnedSection(pinnedEntries);
+
+        // Add unpinned entries section
+        BuildUnpinnedSection(unpinnedEntries);
+    }
+
+    private void BuildPinnedSection(List<Entry> pinnedEntries)
+    {
+        if (pinnedEntries.Count == 0)
+        {
+            return;
+        }
+
+        // Create pinned section header
+        var pinnedHeader = new TextBlock
+        {
+            Text = TranslationManager.Translation.Pinned,
+            Margin = new Thickness(20, 5, 0, 5),
+            FontFamily = new FontFamily("avares://PicView.Avalonia/Assets/Fonts/Roboto-Bold.ttf#Roboto"),
+            Classes = { "txt" }
+        };
+        menuContainer.Children.Add(pinnedHeader);
+
+        // Add pinned entries
+        // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
+        foreach (var entry in pinnedEntries)
+        {
+            var menuItem = new FileHistoryMenuItem(entry, _currentFilePath, viewModel, -1);
+            menuContainer.Children.Add(menuItem);
+        }
+
+        // Add separator between pinned and unpinned sections
+        var separator = new Separator
+        {
+            Margin = new Thickness(10, 7, 20, 7)
+        };
+        menuContainer.Children.Add(separator);
+    }
+
+    private void BuildUnpinnedSection(List<Entry> unpinnedEntries)
+    {
+        if (unpinnedEntries.Count == 0)
+        {
+            return;
+        }
+
+        var max = unpinnedEntries.Count;
+
+        if (isDescendingSorting)
+        {
+            for (var i = 0; i < max; i++)
+            {
+                var menuItem = new FileHistoryMenuItem(unpinnedEntries[i], _currentFilePath, viewModel, i);
+                menuContainer.Children.Add(menuItem);
+            }
+        }
+        else
+        {
+            for (var i = max - 1; i >= 0; i--)
+            {
+                var menuItem = new FileHistoryMenuItem(unpinnedEntries[i], _currentFilePath, viewModel, i);
+                menuContainer.Children.Add(menuItem);
+            }
+        }
+    }
+}

+ 91 - 0
src/PicView.Avalonia/UI/FileHistory/FileHistoryMenuController.cs

@@ -0,0 +1,91 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using PicView.Avalonia.CustomControls;
+using PicView.Avalonia.ViewModels;
+using PicView.Core.FileHistory;
+
+namespace PicView.Avalonia.UI.FileHistory;
+
+/// <summary>
+///     Controls the file history menu functionality
+/// </summary>
+public class FileHistoryMenuController
+{
+    private readonly Button _clearButton;
+    private readonly Panel _menuContainer;
+    private readonly IconButton _sortButton;
+    private readonly MainViewModel _viewModel;
+
+    public FileHistoryMenuController(Panel menuContainer, IconButton sortButton, Button clearButton,
+        MainViewModel viewModel)
+    {
+        _menuContainer = menuContainer;
+        _sortButton = sortButton;
+        _clearButton = clearButton;
+        _viewModel = viewModel;
+
+        // Initialize sort button icon
+        UpdateSortButtonIcon();
+
+        // Setup event handlers
+        _sortButton.Click += OnHistorySortButtonClicked;
+        _clearButton.Click += OnHistoryClearButtonClicked;
+    }
+
+    /// <summary>
+    ///     Updates the file history menu items when the context menu is opened
+    /// </summary>
+    public void UpdateFileHistoryMenu()
+    {
+        if (FileHistoryManager.Count <= 0)
+        {
+            _menuContainer.Children.Clear();
+            return;
+        }
+
+        var fileHistoryBuilder = new FileHistoryMenuBuilder(
+            _menuContainer,
+            _viewModel,
+            FileHistoryManager.IsSortingDescending);
+
+        fileHistoryBuilder.BuildMenu();
+    }
+
+    /// <summary>
+    ///     Updates the sort button icon based on the current sort direction
+    /// </summary>
+    private void UpdateSortButtonIcon()
+    {
+        if (FileHistoryManager.IsSortingDescending)
+        {
+            if (Application.Current.TryGetResource("SortDescImage",
+                    Application.Current.RequestedThemeVariant, out var sortDescImage))
+            {
+                _sortButton.Icon = sortDescImage as DrawingImage;
+            }
+        }
+        else
+        {
+            if (Application.Current.TryGetResource("SortAscImage",
+                    Application.Current.RequestedThemeVariant, out var sortAscImage))
+            {
+                _sortButton.Icon = sortAscImage as DrawingImage;
+            }
+        }
+    }
+
+    private void OnHistorySortButtonClicked(object? sender, RoutedEventArgs e)
+    {
+        FileHistoryManager.IsSortingDescending = !FileHistoryManager.IsSortingDescending;
+        UpdateSortButtonIcon();
+        UpdateFileHistoryMenu();
+    }
+
+    private void OnHistoryClearButtonClicked(object? sender, RoutedEventArgs e)
+    {
+        FileHistoryManager.Clear();
+        _menuContainer.Children.Clear();
+    }
+}

+ 154 - 0
src/PicView.Avalonia/UI/FileHistory/FileHistoryMenuItem.cs

@@ -0,0 +1,154 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.ViewModels;
+using PicView.Avalonia.Views.UC.Buttons;
+using PicView.Core.Extensions;
+using PicView.Core.FileHistory;
+
+namespace PicView.Avalonia.UI.FileHistory;
+
+/// <summary>
+///     Represents a single file history menu item with pin/unpin functionality
+/// </summary>
+public class FileHistoryMenuItem : Panel
+{
+    private const int MaxFilenameLength = 42;
+
+    public FileHistoryMenuItem(Entry entry, string? currentFilePath, MainViewModel viewModel, int index)
+    {
+        var fileLocation = entry.Path;
+        if (string.IsNullOrEmpty(fileLocation))
+        {
+            return;
+        }
+
+        var isSelected = fileLocation == currentFilePath;
+        var filename = Path.GetFileName(fileLocation);
+        var header = filename.Length > MaxFilenameLength ? filename.Shorten(MaxFilenameLength) : filename;
+
+        // Create the pin button with appropriate visibility
+        var pinButton = CreatePinButton(entry, fileLocation);
+
+        // Create the menu item button with file info
+        var menuItemButton = CreateMenuItemButton(header, fileLocation, isSelected, index, viewModel);
+
+        // Add components to the panel
+        Children.Add(menuItemButton);
+        Children.Add(pinButton);
+
+        // Add hover behavior
+        ConfigureHoverBehavior(pinButton);
+
+        // Set tooltip
+        ToolTip.SetTip(menuItemButton, fileLocation);
+    }
+
+    private static PinButton CreatePinButton(Entry entry, string fileLocation)
+    {
+        var pinBtn = new PinButton
+        {
+            Name = "PinBtn",
+            Opacity = 0,
+            Width = 25,
+            HorizontalAlignment = HorizontalAlignment.Right,
+            ZIndex = 1
+        };
+
+        pinBtn.PinBtn.Click += (_, _) => FileHistoryManager.Pin(fileLocation);
+        pinBtn.UnPinBtn.Click += (_, _) => FileHistoryManager.UnPin(fileLocation);
+
+        if (entry.IsPinned)
+        {
+            pinBtn.PinBtn.IsVisible = false;
+            pinBtn.UnPinBtn.IsVisible = true;
+        }
+        else
+        {
+            pinBtn.PinBtn.IsVisible = true;
+            pinBtn.UnPinBtn.IsVisible = false;
+        }
+
+        return pinBtn;
+    }
+
+    private static Button CreateMenuItemButton(string header, string fileLocation, bool isSelected, int index,
+        MainViewModel viewModel)
+    {
+        var item = new Button
+        {
+            Background = Brushes.Transparent,
+            Padding = new Thickness(5, 6),
+            Width = 355
+        };
+
+        if (index < 0)
+        {
+            // Pinned item without index number
+            item.Padding = new Thickness(15, 0, 0, 0);
+            item.Content = new TextBlock
+            {
+                Classes = { "txt" },
+                Text = header,
+                Padding = new Thickness(5, 5, 0, 5)
+            };
+        }
+        else
+        {
+            // Regular item with index number
+            var indexText = new TextBlock
+            {
+                Classes = { "txt" },
+                Text = (index + 1).ToString(),
+                Padding = new Thickness(5, 0, 2, 0)
+            };
+
+            var headerText = new TextBlock
+            {
+                Classes = { "txt" },
+                Text = header,
+                Padding = new Thickness(5, 0, 0, 0)
+            };
+
+            item.Content = new StackPanel
+            {
+                Orientation = Orientation.Horizontal,
+                Children = { indexText, headerText }
+            };
+        }
+
+        if (isSelected)
+        {
+            item.Classes.Add("active");
+        }
+
+        item.Click += async delegate
+        {
+            UIHelper.GetMainView.MainContextMenu.Close();
+            await NavigationManager.LoadPicFromStringAsync(fileLocation, viewModel).ConfigureAwait(false);
+        };
+
+        return item;
+    }
+
+    private void ConfigureHoverBehavior(PinButton pinBtn)
+    {
+        PointerEntered += (_, _) =>
+        {
+            pinBtn.Opacity = 1;
+            if (Application.Current.TryGetResource("AccentColor", Application.Current.RequestedThemeVariant,
+                    out var accentColor))
+            {
+                Background = accentColor as SolidColorBrush;
+            }
+        };
+
+        PointerExited += (_, _) =>
+        {
+            pinBtn.Opacity = 0;
+            Background = Brushes.Transparent;
+        };
+    }
+}

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

@@ -73,7 +73,7 @@
                         <Setter Property="Width" Value="35" />
                         <Setter Property="Height" Value="35" />
                     </Style>
-                    <Style Selector="Button.colorBtn.active">
+                    <Style Selector="Button.colorBtn.activeColorBtn">
                         <Setter Property="BorderThickness" Value="2" />
                         <Setter Property="BorderBrush" Value="#FFFFFF" />
                     </Style>
@@ -182,7 +182,7 @@
                         <Setter Property="BorderThickness" Value="1" />
                         <Setter Property="BorderBrush" Value="{DynamicResource TertiaryBorderColor}" />
                     </Style>
-                    <Style Selector="Button.colorBtn.active">
+                    <Style Selector="Button.colorBtn.activeColorBtn">
                         <Setter Property="BorderThickness" Value="2" />
                         <Setter Property="BorderBrush" Value="#FFFFFF" />
                     </Style>

+ 36 - 36
src/PicView.Avalonia/Views/AppearanceView.axaml.cs

@@ -70,40 +70,40 @@ public partial class AppearanceView : UserControl
         switch ((ColorOptions)Settings.Theme.ColorTheme)
         {
             case ColorOptions.Raspberry:
-                RaspberryButton.Classes.Add("active");
+                RaspberryButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Teal:
-                TealButton.Classes.Add("active");
+                TealButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Emerald:
-                EmeraldButton.Classes.Add("active");
+                EmeraldButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Golden:
-                GoldButton.Classes.Add("active");
+                GoldButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Orange:
-                OrangeButton.Classes.Add("active");
+                OrangeButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Pink:
-                PinkButton.Classes.Add("active");
+                PinkButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Purple:
-                PurpleButton.Classes.Add("active");
+                PurpleButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Red:
-                RedButton.Classes.Add("active");
+                RedButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Ruby:
-                RubyButton.Classes.Add("active");
+                RubyButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Magenta:
-                MagentaButton.Classes.Add("active");
+                MagentaButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Blue:
-                BlueButton.Classes.Add("active");
+                BlueButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Cyan:
-                CyanButton.Classes.Add("active");
+                CyanButton.Classes.Add("activeColorBtn");
                 break;
         }
         
@@ -157,7 +157,7 @@ public partial class AppearanceView : UserControl
 
         foreach (var button in buttons)
         {
-            button.Classes.Remove("active");
+            button.Classes.Remove("activeColorBtn");
         }
     }
     
@@ -167,40 +167,40 @@ public partial class AppearanceView : UserControl
         switch (colorTheme)
         {
             default:
-                BlueButton.Classes.Add("active");
+                BlueButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Pink:
-                PinkButton.Classes.Add("active");
+                PinkButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Orange:
-                OrangeButton.Classes.Add("active");
+                OrangeButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Ruby:
-                RubyButton.Classes.Add("active");
+                RubyButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Red:
-                RedButton.Classes.Add("active");
+                RedButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Teal:
-                TealButton.Classes.Add("active");
+                TealButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Raspberry:
-                RaspberryButton.Classes.Add("active");
+                RaspberryButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Golden:
-                GoldButton.Classes.Add("active");
+                GoldButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Purple:
-                PurpleButton.Classes.Add("active");
+                PurpleButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Cyan:
-                CyanButton.Classes.Add("active");
+                CyanButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Magenta:
-                MagentaButton.Classes.Add("active");
+                MagentaButton.Classes.Add("activeColorBtn");
                 break;
             case ColorOptions.Emerald:
-                EmeraldButton.Classes.Add("active");
+                EmeraldButton.Classes.Add("activeColorBtn");
                 break;
         }
 
@@ -269,34 +269,34 @@ public partial class AppearanceView : UserControl
         switch (selectedBg)
         {
             default:
-                TransparentBgButton.Classes.Add("active");
+                TransparentBgButton.Classes.Add("activeColorBtn");
                 break;
             case 1:
-                NoiseTextureButton.Classes.Add("active");
+                NoiseTextureButton.Classes.Add("activeColorBtn");
                 break;
             case 2:
-                CheckerboardButton.Classes.Add("active");
+                CheckerboardButton.Classes.Add("activeColorBtn");
                 break;
             case 3:
-                CheckerboardAltButton.Classes.Add("active");
+                CheckerboardAltButton.Classes.Add("activeColorBtn");
                 break;
             case 4:
-                WhiteBgButton.Classes.Add("active");
+                WhiteBgButton.Classes.Add("activeColorBtn");
                 break;
             case 5:
-                GrayBgButton.Classes.Add("active");
+                GrayBgButton.Classes.Add("activeColorBtn");
                 break;
             case 6:
-                DarkGrayBgButton.Classes.Add("active");
+                DarkGrayBgButton.Classes.Add("activeColorBtn");
                 break;
             case 7:
-                DarkGraySemiTransparentBgButton.Classes.Add("active");
+                DarkGraySemiTransparentBgButton.Classes.Add("activeColorBtn");
                 break;
             case 8:
-                DarkGraySemiTransparentAltBgButton.Classes.Add("active");
+                DarkGraySemiTransparentAltBgButton.Classes.Add("activeColorBtn");
                 break;
             case 9:
-                BlackBgButton.Classes.Add("active");
+                BlackBgButton.Classes.Add("activeColorBtn");
                 break;
         }
 
@@ -319,7 +319,7 @@ public partial class AppearanceView : UserControl
 
         foreach (var button in buttons)
         {
-            button.Classes.Remove("active");
+            button.Classes.Remove("activeColorBtn");
         }
     }
 }

+ 3 - 2
src/PicView.Avalonia/Views/ImageViewer.axaml

@@ -15,11 +15,12 @@
         <vm:MainViewModel />
     </Design.DataContext>
     <LayoutTransformControl x:Name="ImageLayoutTransformControl">
-        <customControls:MainScrollViewer
+        <customControls:AutoScrollViewer
             Focusable="False"
             Height="{CompiledBinding PicViewer.ScrollViewerHeight,
                                      Mode=OneWay}"
             ScrollChanged="ImageScrollViewer_OnScrollChanged"
+            Theme="{StaticResource Main}"
             VerticalScrollBarVisibility="{CompiledBinding ToggleScrollBarVisibility}"
             Width="{CompiledBinding PicViewer.ScrollViewerWidth,
                                     Mode=OneWay}"
@@ -45,6 +46,6 @@
                     UseLayoutRounding="True"
                     x:Name="MainImage" />
             </Border>
-        </customControls:MainScrollViewer>
+        </customControls:AutoScrollViewer>
     </LayoutTransformControl>
 </UserControl>

+ 1 - 0
src/PicView.Avalonia/Views/LanguageView.axaml

@@ -14,6 +14,7 @@
             Text="{CompiledBinding Translation.Language,
                                    Mode=OneWay}" />
         <ComboBox
+            AutoScrollToSelectedItem="True"
             Background="{DynamicResource SecondaryBackgroundColor}"
             BorderBrush="{DynamicResource MainBorderColor}"
             BorderThickness="1"

+ 72 - 1
src/PicView.Avalonia/Views/MainView.axaml

@@ -10,6 +10,7 @@
     xmlns="https://github.com/avaloniaui"
     xmlns:buttons="clr-namespace:PicView.Avalonia.Views.UC.Buttons"
     xmlns:converters="clr-namespace:PicView.Avalonia.Converters"
+    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:uc="clr-namespace:PicView.Avalonia.Views.UC"
@@ -231,7 +232,7 @@
             </MenuItem>
 
             <!--  Recent files  -->
-            <MenuItem Header="{Binding Translation.RecentFiles, Mode=OneWay}" x:Name="RecentFilesCM">
+            <MenuItem Header="{Binding Translation.RecentFiles, Mode=OneWay}">
                 <MenuItem.Icon>
                     <Path
                         Data="M504 255.531c.253 136.64-111.18 248.372-247.82 248.468-59.015.042-113.223-20.53-155.822-54.911-11.077-8.94-11.905-25.541-1.839-35.607l11.267-11.267c8.609-8.609 22.353-9.551 31.891-1.984C173.062 425.135 212.781 440 256 440c101.705 0 184-82.311 184-184 0-101.705-82.311-184-184-184-48.814 0-93.149 18.969-126.068 49.932l50.754 50.754c10.08 10.08 2.941 27.314-11.313 27.314H24c-8.837 0-16-7.163-16-16V38.627c0-14.254 17.234-21.393 27.314-11.314l49.372 49.372C129.209 34.136 189.552 8 256 8c136.81 0 247.747 110.78 248 247.531zm-180.912 78.784l9.823-12.63c8.138-10.463 6.253-25.542-4.21-33.679L288 256.349V152c0-13.255-10.745-24-24-24h-16c-13.255 0-24 10.745-24 24v135.651l65.409 50.874c10.463 8.137 25.541 6.253 33.679-4.21z"
@@ -240,6 +241,76 @@
                         Stretch="Fill"
                         Width="12" />
                 </MenuItem.Icon>
+                <MenuItem Theme="{StaticResource Slim}" x:Name="RecentFilesHeader">
+                    <MenuItem.Header>
+                        <DockPanel LastChildFill="False">
+                            <Button
+                                Classes="errorHover"
+                                CornerRadius="8,0,0,0"
+                                DockPanel.Dock="Left"
+                                Margin="0,0,5,0"
+                                Padding="8,4"
+                                x:Name="HistoryClearButton">
+                                <StackPanel Orientation="Horizontal">
+                                    <Path
+                                        Data="{StaticResource RecycleGeometry}"
+                                        Fill="{StaticResource Brush0}"
+                                        Height="12"
+                                        Stretch="Fill"
+                                        Width="12" />
+                                    <TextBlock
+                                        Classes="txt"
+                                        Margin="5,0,0,0"
+                                        Text="{CompiledBinding Translation.Clear}" />
+                                </StackPanel>
+                            </Button>
+
+                            <Button
+                                Classes="hover"
+                                DockPanel.Dock="Left"
+                                Margin="0,0,5,0"
+                                Padding="8,4"
+                                x:Name="HistoryFileButton">
+                                <StackPanel Orientation="Horizontal">
+                                    <Path
+                                        Data="{StaticResource FilePropertiesGeometry}"
+                                        Fill="{StaticResource Brush0}"
+                                        Height="12"
+                                        Stretch="Fill"
+                                        Width="12" />
+                                    <TextBlock
+                                        Classes="txt"
+                                        Margin="5,0,0,0"
+                                        Text="{CompiledBinding Translation.OpenFileHistory}" />
+                                </StackPanel>
+                            </Button>
+
+                            <customControls:IconButton
+                                Background="Transparent"
+                                Classes="hover"
+                                CornerRadius="0,8,0,0"
+                                DockPanel.Dock="Right"
+                                Icon="{StaticResource SortDescImage}"
+                                IconHeight="12"
+                                IconWidth="12"
+                                Margin="5,0,0,0"
+                                Padding="8,4"
+                                x:Name="HistorySortButton" />
+
+                        </DockPanel>
+                    </MenuItem.Header>
+                </MenuItem>
+                <Separator Background="{DynamicResource TertiaryBorderColor}" />
+                <MenuItem
+                    StaysOpenOnClick="True"
+                    Theme="{StaticResource Slim}"
+                    x:Name="RecentFilesContent">
+                    <MenuItem.Header>
+                        <customControls:AutoScrollViewer MaxHeight="450" Theme="{StaticResource Inline}">
+                            <StackPanel Margin="0,0,15,0" x:Name="RecentFilesCM" />
+                        </customControls:AutoScrollViewer>
+                    </MenuItem.Header>
+                </MenuItem>
             </MenuItem>
 
             <!--  Settings  -->

+ 34 - 74
src/PicView.Avalonia/Views/MainView.axaml.cs

@@ -3,21 +3,23 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Threading;
 using PicView.Avalonia.Converters;
 using PicView.Avalonia.Crop;
 using PicView.Avalonia.DragAndDrop;
 using PicView.Avalonia.Input;
-using PicView.Avalonia.Navigation;
 using PicView.Avalonia.UI;
+using PicView.Avalonia.UI.FileHistory;
 using PicView.Avalonia.ViewModels;
 using PicView.Avalonia.WindowBehavior;
-using PicView.Core.Extensions;
-using PicView.Core.Navigation;
+using PicView.Core.FileHistory;
 
 namespace PicView.Avalonia.Views;
 
 public partial class MainView : UserControl
 {
+    private FileHistoryMenuController? _historyMenuController;
+    
     public MainView()
     {
         InitializeComponent();
@@ -32,7 +34,7 @@ public partial class MainView : UserControl
             LostFocus += HandleLostFocus;
             PointerPressed += PointerPressedBehavior;
 
-            MainContextMenu.Opened += OnMainContextMenuOpened;
+            MainContextMenu.Opened += async (sender, args) =>  await OnMainContextMenuOpened(sender, args);
 
             if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
@@ -44,10 +46,22 @@ public partial class MainView : UserControl
                 MacOSWallpaperMenuItem.IsVisible = false;
             }
             
+            if (!FileHistoryManager.IsSortingDescending)
+            {
+                if (Application.Current.TryGetResource("SortAscImage",
+                        Application.Current.RequestedThemeVariant, out var sortAscImage))
+                {
+                    HistorySortButton.Icon = sortAscImage as DrawingImage;
+                }
+            }
+            
             if (DataContext is not MainViewModel vm)
             {
                 return;
             }
+            // Initialize the history menu controller
+            _historyMenuController = new FileHistoryMenuController(RecentFilesCM, HistorySortButton, HistoryClearButton, vm);
+
             HideInterfaceLogic.AddHoverButtonEvents(AltButtonsPanel, vm);
             PointerWheelChanged += async (_, e) => await vm.ImageViewer.PreviewOnPointerWheelChanged(this, e);
         };
@@ -87,87 +101,33 @@ public partial class MainView : UserControl
         DragAndDropHelper.RemoveDragDropView();
     }
 
-    private void OnMainContextMenuOpened(object? sender, EventArgs e)
+    private async Task OnMainContextMenuOpened(object? sender, EventArgs e)
     {
         if (DataContext is not MainViewModel vm)
         {
             return;
         }
-
-        CropMenuItem.IsEnabled = CropFunctions.DetermineIfShouldBeEnabled(vm);
-        ConversionHelper.DetermineIfOptimizeImageShouldBeEnabled(vm);
-
-        // Set source for ChangeCtrlZoomImage
-        if (!Application.Current.TryGetResource("ScanEyeImage", Application.Current.RequestedThemeVariant, out var scanEyeImage))
-        {
-            return;
-        }
-        if (!Application.Current.TryGetResource("LeftRightArrowsImage", Application.Current.RequestedThemeVariant, out var leftRightArrowsImage))
+        
+        await Dispatcher.UIThread.InvokeAsync(() =>
         {
-            return;
-        }
-        var isNavigatingWithCtrl = Settings.Zoom.CtrlZoom;
-        vm.ChangeCtrlZoomImage = isNavigatingWithCtrl ? leftRightArrowsImage as DrawingImage : scanEyeImage as DrawingImage;
+            CropMenuItem.IsEnabled = CropFunctions.DetermineIfShouldBeEnabled(vm);
+            ConversionHelper.DetermineIfOptimizeImageShouldBeEnabled(vm);
 
-        // Update file history menu items
-        UpdateFileHistoryMenuItems(vm);
-    }
-
-    private void UpdateFileHistoryMenuItems(MainViewModel vm)
-    {
-        // Clear existing items 
-        RecentFilesCM.Items.Clear();
-        var currentFilePath = NavigationManager.GetCurrentFileName;
-            
-        // Add menu items for each history entry
-        for (var i = 0; i < FileHistory.Count; i++)
-        {
-            var fileLocation = FileHistory.GetEntry(i);
-            if (string.IsNullOrEmpty(fileLocation))
-                continue;
-                
-            var isSelected = fileLocation == currentFilePath;
-            var filename = Path.GetFileNameWithoutExtension(fileLocation);
-            var header = filename.Length > 60 ? filename.Shorten(60) : filename;
-            
-            var item = new MenuItem
-            {
-                Header = header
-            };
-            if (isSelected)
+            // Set source for ChangeCtrlZoomImage
+            if (!Application.Current.TryGetResource("ScanEyeImage", Application.Current.RequestedThemeVariant, out var scanEyeImage))
             {
-                item.Classes.Add("active");
+                return;
             }
-            
-            var filePath = fileLocation; // Local copy for the closure
-            item.Click += async delegate
+            if (!Application.Current.TryGetResource("LeftRightArrowsImage", Application.Current.RequestedThemeVariant, out var leftRightArrowsImage))
             {
-                await NavigationManager.LoadPicFromStringAsync(filePath, vm).ConfigureAwait(false);
-            };
-            
-            ToolTip.SetTip(item, fileLocation);
-            
-            RecentFilesCM.Items.Add(item);
-        }
+                return;
+            }
+            var isNavigatingWithCtrl = Settings.Zoom.CtrlZoom;
+            vm.ChangeCtrlZoomImage = isNavigatingWithCtrl ? leftRightArrowsImage as DrawingImage : scanEyeImage as DrawingImage;
+        }, DispatcherPriority.Render);
         
-        // TODO add clear history translations
-        // Add a separator and "Clear history" option if there are items
-        // if (FileHistory.Count <= 0)
-        // {
-        //     return;
-        // }
-        //
-        // RecentFilesCM.Items.Add(new Separator());
-        //     
-        // var clearItem = new MenuItem { Header = TranslationHelper.GetTranslation("ClearHistory") };
-        // clearItem.Click += delegate
-        // {
-        //     FileHistory.Clear();
-        //     FileHistory.SaveToFile();
-        //     RecentFilesCM.Items.Clear();
-        // };
-        //     
-        // RecentFilesCM.Items.Add(clearItem);
+        // Update file history menu items in Dispatcher with low priority to avoid slowdown
+        await Dispatcher.UIThread.InvokeAsync(() => _historyMenuController?.UpdateFileHistoryMenu(), DispatcherPriority.Background);
     }
 
     private async Task Drop(object? sender, DragEventArgs e)

+ 32 - 0
src/PicView.Avalonia/Views/UC/Buttons/PinButton.axaml

@@ -0,0 +1,32 @@
+<UserControl
+    Background="Transparent"
+    d:DesignHeight="450"
+    d:DesignWidth="800"
+    mc:Ignorable="d"
+    x:Class="PicView.Avalonia.Views.UC.Buttons.PinButton"
+    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"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Panel>
+        <customControls:IconButton
+            Background="Transparent"
+            Icon="{StaticResource PinImage}"
+            IconHeight="12"
+            IconWidth="12"
+            ToolTip.Tip="{CompiledBinding Translation.Pin,
+                                          Mode=OneWay}"
+            x:Name="PinBtn" />
+        <customControls:IconButton
+            Background="Transparent"
+            Icon="{StaticResource UnPinImage}"
+            IconHeight="12"
+            IconWidth="12"
+            ToolTip.Tip="{CompiledBinding Translation.Unpin,
+                                          Mode=OneWay}"
+            x:Name="UnPinBtn" />
+    </Panel>
+</UserControl>

+ 19 - 0
src/PicView.Avalonia/Views/UC/Buttons/PinButton.axaml.cs

@@ -0,0 +1,19 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace PicView.Avalonia.Views.UC.Buttons;
+public partial class PinButton : UserControl
+{
+    public PinButton()
+    {
+        InitializeComponent();
+        PointerEntered += (_, e) =>
+        {
+            Background = new SolidColorBrush { Color = Color.FromArgb(45, 0, 0, 0) };
+        };
+        PointerExited += (_, e) =>
+        {
+            Background = Brushes.Transparent;
+        };
+    }
+}

+ 4 - 4
src/PicView.Avalonia/WindowBehavior/WindowFunctions.cs

@@ -11,7 +11,7 @@ using PicView.Avalonia.UI;
 using PicView.Avalonia.ViewModels;
 using PicView.Core.ArchiveHandling;
 using PicView.Core.FileHandling;
-using PicView.Core.Navigation;
+using PicView.Core.FileHistory;
 using PicView.Core.Sizing;
 
 // ReSharper disable CompareOfFloatsByEqualityOperator
@@ -51,20 +51,20 @@ public static class WindowFunctions
             }
             else
             {
-                lastFile = vm?.PicViewer.FileInfo?.FullName ?? FileHistory.GetLastEntry();
+                lastFile = vm?.PicViewer.FileInfo?.FullName ?? FileHistoryManager.GetLastEntry();
             }
         }
         else
         {
             var url = vm?.PicViewer.Title.GetURL();
-            lastFile = !string.IsNullOrWhiteSpace(url) ? url : FileHistory.GetLastEntry();
+            lastFile = !string.IsNullOrWhiteSpace(url) ? url : FileHistoryManager.GetLastEntry();
         }
 
         Settings.StartUp.LastFile = lastFile;
         await SaveSettingsAsync();
         await KeybindingManager.UpdateKeyBindingsFile(); // Save keybindings
         FileDeletionHelper.DeleteTempFiles();
-        FileHistory.SaveToFile();
+        FileHistoryManager.SaveToFile();
         ArchiveExtraction.Cleanup();
         Environment.Exit(0);
     }

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Klik for at ændre genvejstast",
   "ChangingThemeRequiresRestart": "* Skiftning af tema kræver genstart",
   "CheckForUpdates": "Søg efter opdateringer",
+  "Clear": "Ryd",
   "ClearEffects": "Fjern effekter",
   "ClipboardImage": "Billede fra udklipsholderen",
   "Close": "Luk",
@@ -231,6 +232,7 @@
   "OldMovie": "Gammel film",
   "Open": "Åben",
   "OpenFileDialog": "Åben fil dialog",
+  "OpenFileHistory": "Åbn filhistorik",
   "OpenInSameWindow": "Åbn filer i det samme vindue",
   "OpenLastFile": "Åben seneste fil",
   "OpenWith": "Åben med...",
@@ -245,6 +247,8 @@
   "Percentage": "Procent",
   "PermanentlyDelete": "Slet permanent",
   "PhotometricInterpretation": "Fotometrisk fortolkning",
+  "Pin": "Fastgør",
+  "Pinned": "Fastgjort",
   "Pixels": "pixels",
   "Portrait": "Portræt",
   "Posterize": "Posterize",
@@ -362,6 +366,7 @@
   "Uniform": "Ensartet",
   "UniformToFill": "Ensartet til fyldt",
   "Unknown": "Ukendt",
+  "Unpin": "Frigør",
   "UnselectAll": "Fravælg alle",
   "UnsupportedFile": "Ikke understøttet filformat",
   "Up": "Op",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Klicken, um die Tastenbelegung zu ändern",
   "ChangingThemeRequiresRestart": "* Ändern des Themas erfordert einen Neustart",
   "CheckForUpdates": "Auf Updates prüfen",
+  "Clear": "Löschen",
   "ClearEffects": "Effekte entfernen",
   "ClipboardImage": "Bild in der Zwischenablage",
   "Close": "Schließen",
@@ -231,6 +232,7 @@
   "OldMovie": "Alter Film",
   "Open": "Öffnen",
   "OpenFileDialog": "Datei auswählen",
+  "OpenFileHistory": "Dateiverlauf öffnen",
   "OpenInSameWindow": "Dateien im selben Fenster öffnen",
   "OpenLastFile": "Letzte Datei öffnen",
   "OpenWith": "Öffnen mit...",
@@ -245,6 +247,8 @@
   "Percentage": "Prozentsatz",
   "PermanentlyDelete": "Endgültig löschen",
   "PhotometricInterpretation": "Photometrische Interpretation",
+  "Pin": "Anheften",
+  "Pinned": "Angeheftet",
   "Pixels": "Pixel",
   "Portrait": "Porträt",
   "Posterize": "Posterisieren",
@@ -362,6 +366,7 @@
   "Uniform": "Gleichmäßig",
   "UniformToFill": "Gleichmäßig zum Füllen",
   "Unknown": "Unbekannt",
+  "Unpin": "Lösen",
   "UnselectAll": "Alle abwählen",
   "UnsupportedFile": "Nicht unterstützte Datei",
   "Up": "Hoch",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Click to change keybinding",
   "ChangingThemeRequiresRestart": "* Changing theme requires restart",
   "CheckForUpdates": "Check for updates",
+  "Clear": "Clear",
   "ClearEffects": "Clear effects",
   "ClipboardImage": "Clipboard image",
   "Close": "Close",
@@ -231,6 +232,7 @@
   "OldMovie": "Old Movie",
   "Open": "Open",
   "OpenFileDialog": "Select a file",
+  "OpenFileHistory": "Open file history",
   "OpenInSameWindow": "Open files in the same window",
   "OpenLastFile": "Open last file",
   "OpenWith": "Open with...",
@@ -245,6 +247,8 @@
   "Percentage": "Percentage",
   "PermanentlyDelete": "Permanently delete",
   "PhotometricInterpretation": "Photometric interpretation",
+  "Pin": "Pin",
+  "Pinned": "Pinned",
   "Pixels": "pixels",
   "Portrait": "Portrait",
   "Posterize": "Posterize",
@@ -362,6 +366,7 @@
   "Uniform": "Uniform",
   "UniformToFill": "Uniform to fill",
   "Unknown": "Unknown",
+  "Unpin": "Unpin",
   "UnselectAll": "Unselect all",
   "UnsupportedFile": "Unsupported file",
   "Up": "Up",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Haz clic para cambiar la asignación de teclas",
   "ChangingThemeRequiresRestart": "* Cambiar el tema requiere reabrir el programa",
   "CheckForUpdates": "Compruebe las actualizaciones",
+  "Clear": "Limpiar",
   "ClearEffects": "Borrar efectos",
   "ClipboardImage": "Imagen de Portapapeles",
   "Close": "Cerrar",
@@ -231,6 +232,7 @@
   "OldMovie": "Filme Antiguo",
   "Open": "Abrir",
   "OpenFileDialog": "Abrir selector de archivo",
+  "OpenFileHistory": "Abrir historial de archivos",
   "OpenInSameWindow": "Abrir archivos en la misma ventana",
   "OpenLastFile": "Abrir ultimo archivo",
   "OpenWith": "Abrir con...",
@@ -245,6 +247,8 @@
   "Percentage": "Porcentaje",
   "PermanentlyDelete": "Eliminar permanentemente",
   "PhotometricInterpretation": "Interpretación fotométrica",
+  "Pin": "Fijar",
+  "Pinned": "Fijado",
   "Pixels": "pixeles",
   "Portrait": "Vertical",
   "Posterize": "Posterizar",
@@ -362,6 +366,7 @@
   "Uniform": "Uniforme",
   "UniformToFill": "UniformeParaRellenar",
   "Unknown": "Desconocido",
+  "Unpin": "Desfijar",
   "UnselectAll": "Desseleccionar todo",
   "UnsupportedFile": "Archivo no compatible",
   "Up": "Arriba",

+ 5 - 0
src/PicView.Core/Config/Languages/fr.json

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Cliquez pour changer la liaison de touche",
   "ChangingThemeRequiresRestart": "* La modification du thème nécessite un redémarrage",
   "CheckForUpdates": "Vérifier les mises à jour",
+  "Clear": "Effacer",
   "ClearEffects": "Effacer les effets",
   "ClipboardImage": "Image du presse-papiers",
   "Close": "Fermer",
@@ -231,6 +232,7 @@
   "OldMovie": "Vieille vidéo",
   "Open": "Ouvrir",
   "OpenFileDialog": "Ouvrir la boîte de dialogue du fichier",
+  "OpenFileHistory": "Ouvrir l'historique des fichiers",
   "OpenInSameWindow": "Ouvrir les fichiers dans la même fenêtre",
   "OpenLastFile": "Ouvrir le dernier fichier",
   "OpenWith": "Ouvrir avec...",
@@ -245,6 +247,8 @@
   "Percentage": "Pourcentage",
   "PermanentlyDelete": "Supprimer définitivement",
   "PhotometricInterpretation": "Interprétation photométrique",
+  "Pin": "Épingler",
+  "Pinned": "Épinglé",
   "Pixels": "pixels",
   "Portrait": "Portrait",
   "Posterize": "Postériser",
@@ -362,6 +366,7 @@
   "Uniform": "Uniforme",
   "UniformToFill": "UniformePourRemplir",
   "Unknown": "Inconnu",
+  "Unpin": "Désépingler",
   "UnselectAll": "Tout désélectionner",
   "UnsupportedFile": "Fichier non pris en charge",
   "Up": "Haut",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Clicca per cambiare l'assegnazione del tasto",
   "ChangingThemeRequiresRestart": "* Il cambio di tema richiede il riavvio",
   "CheckForUpdates": "Controlla gli aggiornamenti",
+  "Clear": "Cancella",
   "ClearEffects": "Cancella effetti",
   "ClipboardImage": "Immagine negli appunti",
   "Close": "Chiudere",
@@ -231,6 +232,7 @@
   "OldMovie": "Vecchio film",
   "Open": "Aprire",
   "OpenFileDialog": "Seleziona un file",
+  "OpenFileHistory": "Apri cronologia dei file",
   "OpenInSameWindow": "Apri i file nella stessa finestra",
   "OpenLastFile": "Apri l'ultimo file",
   "OpenWith": "Aprire con...",
@@ -245,6 +247,8 @@
   "Percentage": "Percentuale",
   "PermanentlyDelete": "Elimina definitivamente",
   "PhotometricInterpretation": "Interpretazione fotometrica",
+  "Pin": "Appunta",
+  "Pinned": "Appuntato",
   "Pixels": "Pixel",
   "Portrait": "Ritratto",
   "Posterize": "Posterizzare",
@@ -362,6 +366,7 @@
   "Uniform": "Uniforme",
   "UniformToFill": "UniformePerRiempire",
   "Unknown": "Sconosciuto",
+  "Unpin": "Rimuovi appuntatura",
   "UnselectAll": "Deseleziona tutto",
   "UnsupportedFile": "File non supportato",
   "Up": "Su",

+ 5 - 0
src/PicView.Core/Config/Languages/ja.json

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "クリックしてキーバインドを変更",
   "ChangingThemeRequiresRestart": "* テーマを変更するには再起動が必要です",
   "CheckForUpdates": "更新の確認",
+  "Clear": "クリア",
   "ClearEffects": "効果を消去",
   "ClipboardImage": "クリップボード画像",
   "Close": "閉じる",
@@ -231,6 +232,7 @@
   "OldMovie": "古い映画",
   "Open": "開く",
   "OpenFileDialog": "ファイルの選択",
+  "OpenFileHistory": "ファイル履歴を開く。",
   "OpenInSameWindow": "同じウィンドウへファイルを開く",
   "OpenLastFile": "最後に使ったファイルを開く",
   "OpenWith": "アプリで開く...",
@@ -245,6 +247,8 @@
   "Percentage": "パーセンテージ",
   "PermanentlyDelete": "完全に削除",
   "PhotometricInterpretation": "測光解釈",
+  "Pin": "固定",
+  "Pinned": "固定済み",
   "Pixels": "ピクセル",
   "Portrait": "縦向き",
   "Posterize": "ポスタライズ",
@@ -362,6 +366,7 @@
   "Uniform": "均一",
   "UniformToFill": "均一塗りつぶし",
   "Unknown": "不明",
+  "Unpin": "固定解除",
   "UnselectAll": "すべて選択解除",
   "UnsupportedFile": "サポートされていないファイル",
   "Up": "上",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "키 바인딩을 변경하려면 클릭",
   "ChangingThemeRequiresRestart": "* 테마 변경 시 재시작 필요",
   "CheckForUpdates": "업데이트 확인",
+  "Clear": "지우기",
   "ClearEffects": "효과 지우기",
   "ClipboardImage": "클립보드 이미지",
   "Close": "닫기",
@@ -231,6 +232,7 @@
   "OldMovie": "오래된 영화",
   "Open": "열기",
   "OpenFileDialog": "파일 열기 대화 상자",
+  "OpenFileHistory": "파일 기록 열기",
   "OpenInSameWindow": "동일한 창에서 파일 열기",
   "OpenLastFile": "마지막 파일 열기",
   "OpenWith": "열기...",
@@ -245,6 +247,8 @@
   "Percentage": "백분율",
   "PermanentlyDelete": "영구 삭제",
   "PhotometricInterpretation": "측광 해석",
+  "Pin": "고정",
+  "Pinned": "고정됨",
   "Pixels": "픽셀",
   "Portrait": "세로",
   "Posterize": "포스터라이즈",
@@ -362,6 +366,7 @@
   "Uniform": "균일하게",
   "UniformToFill": "균일하게 채우기",
   "Unknown": "알 수 없음",
+  "Unpin": "고정 해제",
   "UnselectAll": "모두 선택 해제",
   "UnsupportedFile": "지원되지 않는 파일",
   "Up": "위로",

+ 5 - 0
src/PicView.Core/Config/Languages/nl.json

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Klik om de toetsbinding te wijzigen",
   "ChangingThemeRequiresRestart": "* Thema wijzigen vereist herstart",
   "CheckForUpdates": "Controleren op updates",
+  "Clear": "Wissen",
   "ClearEffects": "Effecten wissen",
   "ClipboardImage": "Klembord afbeelding",
   "Close": "Sluiten",
@@ -231,6 +232,7 @@
   "OldMovie": "Oude film",
   "Open": "Openen",
   "OpenFileDialog": "Selecteer een bestand",
+  "OpenFileHistory": "Bestandsgeschiedenis openen",
   "OpenInSameWindow": "Bestanden openen in hetzelfde venster",
   "OpenLastFile": "Laatste bestand openen",
   "OpenWith": "Openen met...",
@@ -245,6 +247,8 @@
   "Percentage": "Percentage",
   "PermanentlyDelete": "Permanent verwijderen",
   "PhotometricInterpretation": "Fotometrische interpretatie",
+  "Pin": "Vastzetten",
+  "Pinned": "Vastgezet",
   "Pixels": "pixels",
   "Portrait": "Portret",
   "Posterize": "Posteriseren",
@@ -362,6 +366,7 @@
   "Uniform": "Uniform",
   "UniformToFill": "Uniform vullen",
   "Unknown": "Onbekend",
+  "Unpin": "Losmaken",
   "UnselectAll": "Alles deselecteren",
   "UnsupportedFile": "Niet ondersteund bestand",
   "Up": "Omhoog",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Kliknij: aby zmienić przypisanie klawisza",
   "ChangingThemeRequiresRestart": "* Zmiana motywu wymaga restartu programu",
   "CheckForUpdates": "Sprawdź aktualizacje",
+  "Clear": "Wyczyść",
   "ClearEffects": "Usuń efekty",
   "ClipboardImage": "Zdjęcie ze schowka",
   "Close": "Zamknij",
@@ -231,6 +232,7 @@
   "OldMovie": "Stary film",
   "Open": "Otwórz",
   "OpenFileDialog": "Wybierz plik",
+  "OpenFileHistory": "Otwórz historię plików",
   "OpenInSameWindow": "Otwórz pliki w tym samym oknie",
   "OpenLastFile": "Otwórz ostatni plik",
   "OpenWith": "Otwórz przy pomocy...",
@@ -245,6 +247,8 @@
   "Percentage": "Procent",
   "PermanentlyDelete": "Usuń trwale",
   "PhotometricInterpretation": "Interpretacja fotometryczna",
+  "Pin": "Przypnij",
+  "Pinned": "Przypięte",
   "Pixels": "pikseli",
   "Portrait": "Portretowy",
   "Posterize": "Posteryzacja",
@@ -362,6 +366,7 @@
   "Uniform": "Jednolity",
   "UniformToFill": "JednolityDoWypełnienia",
   "Unknown": "Nieznane",
+  "Unpin": "Odepnij",
   "UnselectAll": "Odznacz wszystko",
   "UnsupportedFile": "Nieobsługiwany plik",
   "Up": "Góra",

+ 5 - 0
src/PicView.Core/Config/Languages/pt-br.json

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Clique para alterar a tecla de atalho",
   "ChangingThemeRequiresRestart": "* Mudar o tema requer reinicialização",
   "CheckForUpdates": "Verificar atualizações",
+  "Clear": "Limpar",
   "ClearEffects": "Limpar efeitos",
   "ClipboardImage": "Imagem da área de transferência",
   "Close": "Fechar",
@@ -231,6 +232,7 @@
   "OldMovie": "Filme antigo",
   "Open": "Abrir",
   "OpenFileDialog": "Escolha um arquivo",
+  "OpenFileHistory": "Abrir histórico de arquivos",
   "OpenInSameWindow": "Abrir arquivos na mesma janela",
   "OpenLastFile": "Abrir o último arquivo",
   "OpenWith": "Abrir com...",
@@ -245,6 +247,8 @@
   "Percentage": "Porcentagem",
   "PermanentlyDelete": "Excluir permanentemente",
   "PhotometricInterpretation": "Interpretação fotométrica",
+  "Pin": "Fixar",
+  "Pinned": "Fixado",
   "Pixels": "pixels",
   "Portrait": "Retrato",
   "Posterize": "Posterizar",
@@ -362,6 +366,7 @@
   "Uniform": "Uniforme",
   "UniformToFill": "Igualmente preenchida",
   "Unknown": "Desconhecido",
+  "Unpin": "Desafixar",
   "UnselectAll": "Desmarcar todos",
   "UnsupportedFile": "Arquivo não suportado",
   "Up": "Para cima",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Click pentru a schimba atribuirea tastei",
   "ChangingThemeRequiresRestart": "* Modificarea temei necesită repornire",
   "CheckForUpdates": "Caută actualizări",
+  "Clear": "Șterge",
   "ClearEffects": "Șterge efectele",
   "ClipboardImage": "Imagine din memoria temporară",
   "Close": "Închidere",
@@ -231,6 +232,7 @@
   "OldMovie": "Film vechi",
   "Open": "Deschidere",
   "OpenFileDialog": "Selectează un fișier",
+  "OpenFileHistory": "Deschide istoricul fișierelor",
   "OpenInSameWindow": "Deschideți fișierele în aceeași fereastră",
   "OpenLastFile": "Deschide ultimul fișier",
   "OpenWith": "Deschidere cu....",
@@ -245,6 +247,8 @@
   "Percentage": "Procentaj",
   "PermanentlyDelete": "Șterge definitiv",
   "PhotometricInterpretation": "Interpretare fotometrică",
+  "Pin": "Fixează",
+  "Pinned": "Fixat",
   "Pixels": "pixeli",
   "Portrait": "Portret",
   "Posterize": "Posterize",
@@ -362,6 +366,7 @@
   "Uniform": "Uniform",
   "UniformToFill": "UniformPentruUmplere",
   "Unknown": "Necunoscut",
+  "Unpin": "Anulează fixarea",
   "UnselectAll": "Deselectează tot",
   "UnsupportedFile": "Fișier neacceptat",
   "Up": "Sus",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Щелкните, чтобы изменить назначение клавиши",
   "ChangingThemeRequiresRestart": "* Для изменения темы требуется перезапуск",
   "CheckForUpdates": "Проверить наличие обновлений",
+  "Clear": "Очистить",
   "ClearEffects": "Очистить эффекты",
   "ClipboardImage": "Изображение из буфера обмена",
   "Close": "Закрыть",
@@ -231,6 +232,7 @@
   "OldMovie": "Старый фильм",
   "Open": "Открыть",
   "OpenFileDialog": "Выберать файл",
+  "OpenFileHistory": "Открыть историю файлов",
   "OpenInSameWindow": "Открывать файлы в том же окне",
   "OpenLastFile": "Открыть последний файл",
   "OpenWith": "Открыть вместе с...",
@@ -245,6 +247,8 @@
   "Percentage": "Процент",
   "PermanentlyDelete": "Удалить навсегда",
   "PhotometricInterpretation": "Фотометрическая интерпретация",
+  "Pin": "Закрепить",
+  "Pinned": "Закреплено",
   "Pixels": "пиксели",
   "Portrait": "Портрет",
   "Posterize": "Постеризация",
@@ -362,6 +366,7 @@
   "Uniform": "Равномерный",
   "UniformToFill": "РавномерныйДляЗаполнения",
   "Unknown": "Неизвестно",
+  "Unpin": "Открепить",
   "UnselectAll": "Снять все",
   "UnsupportedFile": "Неподдерживаемый файл",
   "Up": "Вверх",

+ 5 - 0
src/PicView.Core/Config/Languages/sv.json

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Klicka för att associera tangent",
   "ChangingThemeRequiresRestart": "* Ändring av tema kräver omstart av programmet",
   "CheckForUpdates": "Sök efter uppdateringar",
+  "Clear": "Rensa",
   "ClearEffects": "Rensa effekter",
   "ClipboardImage": "Urklippsbild",
   "Close": "Stäng",
@@ -231,6 +232,7 @@
   "OldMovie": "Gammal film",
   "Open": "Öppna",
   "OpenFileDialog": "Välj fil",
+  "OpenFileHistory": "Öppna filhistorik",
   "OpenInSameWindow": "Öppna filer i samma fönster",
   "OpenLastFile": "Öppna senaste filen",
   "OpenWith": "Öppna med...",
@@ -245,6 +247,8 @@
   "Percentage": "Procent",
   "PermanentlyDelete": "Ta bort permanent",
   "PhotometricInterpretation": "Fotometrisk tolkning",
+  "Pin": "Fäst",
+  "Pinned": "Fäst",
   "Pixels": "pixlar",
   "Portrait": "Stående",
   "Posterize": "Posterize",
@@ -362,6 +366,7 @@
   "Uniform": "Enhetlig",
   "UniformToFill": "Enhetlig att fylla",
   "Unknown": "Okänd",
+  "Unpin": "Ta bort fästning",
   "UnselectAll": "Avmarkera alla",
   "UnsupportedFile": "Filformatet stöds inte",
   "Up": "Upp",

+ 5 - 0
src/PicView.Core/Config/Languages/tr.json

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "Kısayolu değiştirmek için tıkla",
   "ChangingThemeRequiresRestart": "* Tema değişikliği yeniden başlatmayı gerektirir",
   "CheckForUpdates": "Güncellemeleri kontrol et",
+  "Clear": "Temizle",
   "ClearEffects": "Efektleri temizle",
   "ClipboardImage": "Pano resmi",
   "Close": "Kapat",
@@ -231,6 +232,7 @@
   "OldMovie": "Eski Film",
   "Open": "Aç",
   "OpenFileDialog": "Bir dosya seçin",
+  "OpenFileHistory": "Dosya geçmişini aç",
   "OpenInSameWindow": "Dosyaları aynı pencerede aç",
   "OpenLastFile": "Son dosyayı aç",
   "OpenWith": "Şununla aç...",
@@ -245,6 +247,8 @@
   "Percentage": "Yüzde",
   "PermanentlyDelete": "Kalıcı olarak sil",
   "PhotometricInterpretation": "Fotometrik yorumlama",
+  "Pin": "Sabitle",
+  "Pinned": "Sabitlenmiş",
   "Pixels": "pikseller",
   "Portrait": "Portre",
   "Posterize": "Posteri",
@@ -362,6 +366,7 @@
   "Uniform": "üniforma",
   "UniformToFill": "Doldurulacak üniforma",
   "Unknown": "Bilinmiyor",
+  "Unpin": "Sabitlemeyi kaldır",
   "UnselectAll": "Tümünü Seçimi Kaldır",
   "UnsupportedFile": "Desteklenmeyen dosya",
   "Up": "Yukarı",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "点击更改键绑定",
   "ChangingThemeRequiresRestart": "* 切换主题需要重新启动应用程序",
   "CheckForUpdates": "检查更新",
+  "Clear": "清除",
   "ClearEffects": "清除效果",
   "ClipboardImage": "剪贴板图片",
   "Close": "关闭",
@@ -231,6 +232,7 @@
   "OldMovie": "老电影",
   "Open": "打开",
   "OpenFileDialog": "打开文件对话框",
+  "OpenFileHistory": "打开文件历史记录",
   "OpenInSameWindow": "在同一窗口中打开文件",
   "OpenLastFile": "打开最后一个文件",
   "OpenWith": "打开使用",
@@ -245,6 +247,8 @@
   "Percentage": "百分比",
   "PermanentlyDelete": "永久删除",
   "PhotometricInterpretation": "光度学解释",
+  "Pin": "固定",
+  "Pinned": "已固定",
   "Pixels": "px",
   "Portrait": "纵向",
   "Posterize": "色调分离",
@@ -362,6 +366,7 @@
   "Uniform": "均匀",
   "UniformToFill": "均匀填充",
   "Unknown": "未知",
+  "Unpin": "取消固定",
   "UnselectAll": "取消全选",
   "UnsupportedFile": "不支持的文件格式",
   "Up": "上",

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

@@ -45,6 +45,7 @@
   "ChangeKeybindingTooltip": "點擊更改鍵綁定",
   "ChangingThemeRequiresRestart": "* 切換主題需要重新啟動應用程式",
   "CheckForUpdates": "檢查更新",
+  "Clear": "清除",
   "ClearEffects": "清除效果",
   "ClipboardImage": "剪貼簿圖片",
   "Close": "關閉",
@@ -231,6 +232,7 @@
   "OldMovie": "老電影",
   "Open": "開啟",
   "OpenFileDialog": "開啟檔案對話方塊",
+  "OpenFileHistory": "開啟檔案歷史記錄",
   "OpenInSameWindow": "在同一視窗中開啟檔案",
   "OpenLastFile": "開啟最後一個檔案",
   "OpenWith": "開啟使用",
@@ -245,6 +247,8 @@
   "Percentage": "百分比",
   "PermanentlyDelete": "永久刪除",
   "PhotometricInterpretation": "光度解釋",
+  "Pin": "固定",
+  "Pinned": "已固定",
   "Pixels": "像素",
   "Portrait": "縱向",
   "Posterize": "色調分離",
@@ -362,6 +366,7 @@
   "Uniform": "均勻",
   "UniformToFill": "均勻填充",
   "Unknown": "未知",
+  "Unpin": "取消固定",
   "UnselectAll": "全部取消選取",
   "UnsupportedFile": "不支持的文件格式",
   "Up": "上",

+ 15 - 0
src/PicView.Core/Config/SettingsConfiguration.cs

@@ -0,0 +1,15 @@
+namespace PicView.Core.Config;
+
+public static class SettingsConfiguration
+{
+    public const double CurrentSettingsVersion = 1.3;
+    
+    public const string ConfigFileName = "UserSettings.json";
+    public const string LocalConfigFilePath = "Config/" + ConfigFileName;
+    public const string RoamingConfigFolder = "Ruben2776/PicView/Config";
+    public const string RoamingConfigPath = RoamingConfigFolder + "/" + ConfigFileName;
+    
+    public const string HistoryFileName = "FileHistory.json";
+    public const string LocalHistoryFilePath = "Config/" + HistoryFileName;
+    public const string RoamingFileHistoryPath = RoamingConfigFolder + "/" + HistoryFileName;
+}

+ 19 - 16
src/PicView.Core/Config/SettingsManager.cs

@@ -8,16 +8,10 @@ namespace PicView.Core.Config;
 
 [JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)]
 [JsonSerializable(typeof(AppSettings))]
-internal partial class SourceGenerationContext : JsonSerializerContext;
+internal partial class SettingsGenerationContext : JsonSerializerContext;
 
 public static class SettingsManager
 {
-    private const double CurrentSettingsVersion = 1.3;
-    private const string ConfigFileName = "UserSettings.json";
-    private const string LocalConfigPath = "Config/" + ConfigFileName;
-    private const string RoamingConfigFolder = "Ruben2776/PicView/Config";
-    private const string RoamingConfigPath = RoamingConfigFolder + "/" + ConfigFileName;
-    
     public static string? CurrentSettingsPath { get; private set; }
 
     public static AppSettings? Settings { get; private set; }
@@ -71,7 +65,7 @@ public static class SettingsManager
     /// </summary>
     private static string GetRoamingSettingsPath()
     {
-        return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), RoamingConfigPath);
+        return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), SettingsConfiguration.RoamingConfigPath);
     }
 
     /// <summary>
@@ -79,7 +73,7 @@ public static class SettingsManager
     /// </summary>
     private static string GetLocalSettingsPath()
     {
-        return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LocalConfigPath);
+        return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, SettingsConfiguration.LocalConfigFilePath);
     }
 
     /// <summary>
@@ -97,7 +91,7 @@ public static class SettingsManager
             WindowProperties = new WindowProperties(),
             Zoom = new Zoom(),
             StartUp = new StartUp(),
-            Version = CurrentSettingsVersion
+            Version = SettingsConfiguration.CurrentSettingsVersion
         };
         // Get the default culture from the OS
         Settings.UIProperties.UserLanguage = CultureInfo.CurrentCulture.Name;
@@ -176,7 +170,7 @@ public static class SettingsManager
         var jsonString = await File.ReadAllTextAsync(path).ConfigureAwait(false);
 
         if (JsonSerializer.Deserialize(
-                jsonString, typeof(AppSettings), SourceGenerationContext.Default) is not AppSettings settings)
+                jsonString, typeof(AppSettings), SettingsGenerationContext.Default) is not AppSettings settings)
         {
             throw new JsonException("Failed to deserialize settings");
         }
@@ -250,7 +244,7 @@ public static class SettingsManager
             return;
         }
 
-        var json = JsonSerializer.Serialize(Settings, typeof(AppSettings), SourceGenerationContext.Default);
+        var json = JsonSerializer.Serialize(Settings, typeof(AppSettings), SettingsGenerationContext.Default);
         await File.WriteAllTextAsync(path, json).ConfigureAwait(false);
     }
 
@@ -259,13 +253,13 @@ public static class SettingsManager
     /// </summary>
     private static async Task<AppSettings> UpgradeSettingsIfNeededAsync(AppSettings settings)
     {
-        if (settings.Version >= CurrentSettingsVersion)
+        if (settings.Version >= SettingsConfiguration.CurrentSettingsVersion)
         {
             return settings;
         }
 
         await SynchronizeSettingsAsync(settings).ConfigureAwait(false);
-        settings.Version = CurrentSettingsVersion;
+        settings.Version = SettingsConfiguration.CurrentSettingsVersion;
 
         return settings;
     }
@@ -286,7 +280,7 @@ public static class SettingsManager
             var jsonString = await File.ReadAllTextAsync(localPath).ConfigureAwait(false);
 
             if (JsonSerializer.Deserialize(
-                    jsonString, typeof(AppSettings), SourceGenerationContext.Default) is not AppSettings existingSettings)
+                    jsonString, typeof(AppSettings), SettingsGenerationContext.Default) is not AppSettings existingSettings)
             {
                 return;
             }
@@ -310,10 +304,19 @@ public static class SettingsManager
     /// </summary>
     private static void MergeSettings(AppSettings existingSettings, AppSettings newSettings)
     {
+        existingSettings.UIProperties ??= newSettings.UIProperties;
+        existingSettings.Gallery ??= newSettings.Gallery;
+        existingSettings.Theme ??= newSettings.Theme;
+        existingSettings.Sorting ??= newSettings.Sorting;
+        existingSettings.ImageScaling ??= newSettings.ImageScaling;
+        existingSettings.WindowProperties ??= newSettings.WindowProperties;
+        existingSettings.Zoom ??= newSettings.Zoom;
+        existingSettings.StartUp ??= newSettings.StartUp;
+    
+        // Fallback for any properties missing in older versions
         foreach (var property in typeof(AppSettings).GetProperties())
         {
             var existingValue = property.GetValue(existingSettings);
-
             if (existingValue != null)
             {
                 continue;

+ 15 - 0
src/PicView.Core/FileHistory/FileHistoryEntries.cs

@@ -0,0 +1,15 @@
+namespace PicView.Core.FileHistory;
+
+public class FileHistoryEntries
+{
+    public bool IsSortingDescending { get; set; } = true;
+    
+    public List<Entry> Entries { get; set; }
+}
+
+public class Entry
+{
+    public required string Path { get; set; }
+    
+    public bool IsPinned { get; set; }
+}

+ 131 - 71
src/PicView.Core/Navigation/FileHistory.cs → src/PicView.Core/FileHistory/FileHistoryManager.cs

@@ -1,16 +1,25 @@
 using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 using PicView.Core.ArchiveHandling;
+using PicView.Core.Config;
 using PicView.Core.FileHandling;
 
-namespace PicView.Core.Navigation;
+namespace PicView.Core.FileHistory;
+
+[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)]
+[JsonSerializable(typeof(FileHistoryEntries))]
+[JsonSerializable(typeof(List<Entry>))]
+internal partial class FileHistoryGenerationContext : JsonSerializerContext;
 
 /// <summary>
 ///     Manages the history of recently accessed files.
 /// </summary>
-public static class FileHistory
+public static class FileHistoryManager
 {
-    private const int MaxHistoryEntries = 15;
-    private static readonly List<string> Entries = new(MaxHistoryEntries);
+    private const int MaxHistoryEntries = 50;
+    private const int MaxPinnedEntries = 5;
+    private static readonly List<Entry> Entries = [];
     private static string? _fileLocation;
 
     // ReSharper disable once ReplaceWithFieldKeyword
@@ -21,10 +30,12 @@ public static class FileHistory
     /// </summary>
     public static int Count => Entries.Count;
 
+    public static bool IsSortingDescending { get; set; }
+
     /// <summary>
     ///     Gets all history entries.
     /// </summary>
-    public static IReadOnlyList<string> AllEntries => Entries.AsReadOnly();
+    public static IReadOnlyList<Entry> AllEntries => Entries.AsReadOnly();
 
     /// <summary>
     ///     Gets or sets the current index position in history.
@@ -32,7 +43,7 @@ public static class FileHistory
     public static int CurrentIndex
     {
         get => _currentIndex;
-        private set => _currentIndex = Math.Clamp(value, -1, Entries.Count - 1);
+        private set => _currentIndex = Math.Clamp(value, -1, Count - 1);
     }
 
     /// <summary>
@@ -43,13 +54,13 @@ public static class FileHistory
     /// <summary>
     ///     Indicates whether there is a next entry available in history (newer entry).
     /// </summary>
-    public static bool HasNext => CurrentIndex < Entries.Count - 1 && Entries.Count > 0;
+    public static bool HasNext => CurrentIndex < Count - 1 && Count > 0;
 
     /// <summary>
     ///     Gets the current entry at the current index.
     /// </summary>
     public static string? CurrentEntry =>
-        CurrentIndex >= 0 && CurrentIndex < Entries.Count ? Entries[CurrentIndex] : null;
+        CurrentIndex >= 0 && CurrentIndex < Count ? Entries[CurrentIndex].Path : null;
 
     /// <summary>
     ///     Initializes the file history by loading entries from the history file.
@@ -59,7 +70,7 @@ public static class FileHistory
     /// </remarks>
     public static void Initialize()
     {
-        _fileLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config/recent.txt");
+        _fileLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, SettingsConfiguration.LocalHistoryFilePath);
         try
         {
             if (!File.Exists(_fileLocation))
@@ -74,7 +85,7 @@ public static class FileHistory
             {
                 // TODO: test on macOS
                 _fileLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
-                    "Ruben2776/PicView/Config/recent.txt");
+                    SettingsConfiguration.RoamingFileHistoryPath);
                 if (!File.Exists(_fileLocation))
                 {
                     CreateFile();
@@ -94,8 +105,8 @@ public static class FileHistory
 
         LoadFromFile();
         // Set the current index to the most recent entry.
-        CurrentIndex = Entries.Count > 0 ? Entries.Count - 1 : -1;
-        
+        CurrentIndex = Count > 0 ? Count - 1 : -1;
+
         return;
 
         void CreateFile()
@@ -111,21 +122,62 @@ public static class FileHistory
         }
     }
 
+    public static void Pin(string path) =>
+        Pin(path, true);
+
+    public static void UnPin(string path) =>
+        Pin(path, false);
+
+    private static void Pin(string path, bool isPinned)
+    {
+        var entryIndex = Entries.FindIndex(x => x.Path == path);
+        if (entryIndex < 0)
+        {
+            return;
+        }
+
+        // If already in the desired state, do nothing
+        if (Entries[entryIndex].IsPinned == isPinned)
+        {
+            return;
+        }
+
+        // If trying to pin and we already have max pinned entries, don't allow it
+        if (isPinned && Entries.Count(e => e.IsPinned) >= MaxPinnedEntries)
+        {
+            // Unpin the oldest pinned entry to make room
+            var oldestPinned = Entries.Where(e => e.IsPinned).OrderBy(e => Entries.IndexOf(e)).FirstOrDefault();
+            if (oldestPinned != null)
+            {
+                var oldestIndex = Entries.IndexOf(oldestPinned);
+                Entries[oldestIndex].IsPinned = false;
+            }
+        }
+
+        Entries[entryIndex].IsPinned = isPinned;
+    }
+
     /// <summary>
     ///     Adds an entry to the history.
     /// </summary>
     public static void Add(string path)
     {
-        // Don't add if browsing an archive.
-        if (string.IsNullOrWhiteSpace(path) 
-            || !string.IsNullOrWhiteSpace(ArchiveExtraction.TempZipDirectory)
-            || !string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath))
+        if (string.IsNullOrWhiteSpace(path))
         {
             return;
         }
 
+        // Don't add if browsing an archive, unless the file is an archive itself.
+        if (!string.IsNullOrWhiteSpace(ArchiveExtraction.TempZipDirectory))
+        {
+            if (!path.IsArchive())
+            {
+                return;
+            }
+        }
+
         // Check if the entry already exists.
-        var existingIndex = Entries.IndexOf(path);
+        var existingIndex = Entries.FindIndex(x => x.Path == path);
 
         if (existingIndex >= 0)
         {
@@ -134,20 +186,34 @@ public static class FileHistory
             return;
         }
 
-        // Trim the list if it will exceed the maximum size.
-        if (Entries.Count >= MaxHistoryEntries)
+        // Count unpinned entries
+        var unpinnedCount = Entries.Count(e => !e.IsPinned);
+
+        // If we'll exceed the maximum unpinned entries, remove the oldest unpinned entry
+        if (unpinnedCount >= MaxHistoryEntries)
         {
-            // Remove oldest entry (at beginning).
-            Entries.RemoveAt(0);
-            // Adjust current index since we removed an item.
-            if (CurrentIndex > 0)
+            // Find the oldest unpinned entry
+            for (var i = 0; i < Entries.Count; i++)
             {
-                CurrentIndex--;
+                if (Entries[i].IsPinned)
+                {
+                    continue;
+                }
+
+                Entries.RemoveAt(i);
+
+                // Adjust current index since we removed an item.
+                if (CurrentIndex > i)
+                {
+                    CurrentIndex--;
+                }
+
+                break;
             }
         }
 
         // Add to the end of the list (newest entry).
-        Entries.Add(path);
+        Entries.Add(new Entry { Path = path });
 
         // Set the current index to the newly added item (last position).
         CurrentIndex = Entries.Count - 1;
@@ -184,7 +250,7 @@ public static class FileHistory
     /// <summary>
     ///     Gets an entry at the specified index.
     /// </summary>
-    public static string? GetEntry(int index)
+    public static Entry? GetEntry(int index)
     {
         if (index < 0 || index >= Entries.Count)
         {
@@ -197,17 +263,17 @@ public static class FileHistory
     /// <summary>
     ///     Gets the first entry in history (oldest).
     /// </summary>
-    public static string? GetFirstEntry() => Entries.Count > 0 ? Entries[0] : null;
+    public static string? GetFirstEntry() => Entries.Count > 0 ? Entries[0].Path : null;
 
     /// <summary>
     ///     Gets the last entry in history (newest).
     /// </summary>
-    public static string? GetLastEntry() => Entries.Count > 0 ? Entries[^1] : null;
+    public static string? GetLastEntry() => Entries.Count > 0 ? Entries[^1].Path : null;
 
     /// <summary>
     ///     Tries to find an entry that matches or contains the given string.
     /// </summary>
-    public static string? GetEntryByString(string searchString)
+    public static Entry? GetEntryByString(string searchString)
     {
         if (string.IsNullOrWhiteSpace(searchString))
         {
@@ -216,7 +282,7 @@ public static class FileHistory
 
         // First try exact match.
         var exactMatch = Entries.FirstOrDefault(e =>
-            string.Equals(e, searchString, StringComparison.OrdinalIgnoreCase));
+            string.Equals(e.Path, searchString, StringComparison.OrdinalIgnoreCase));
 
         if (exactMatch != null)
         {
@@ -224,8 +290,8 @@ public static class FileHistory
         }
 
         // Then try contains.
-        return Entries.FirstOrDefault(e =>
-            e.Contains(searchString, StringComparison.OrdinalIgnoreCase));
+        return Entries.Find(e => e.Path.Contains(searchString, StringComparison.OrdinalIgnoreCase)) ??
+               null;
     }
 
     /// <summary>
@@ -242,7 +308,7 @@ public static class FileHistory
     /// </summary>
     public static bool Remove(string path)
     {
-        var index = Entries.IndexOf(path);
+        var index = Entries.FindIndex(e => e.Path == path);
         if (index < 0)
         {
             return false;
@@ -259,27 +325,6 @@ public static class FileHistory
         return true;
     }
 
-    /// <summary>
-    ///     Removes an entry at the specified index.
-    /// </summary>
-    public static bool RemoveAt(int index)
-    {
-        if (index < 0 || index >= Entries.Count)
-        {
-            return false;
-        }
-
-        Entries.RemoveAt(index);
-
-        // Adjust current index if necessary.
-        if (index <= CurrentIndex)
-        {
-            CurrentIndex = Math.Max(-1, CurrentIndex - 1);
-        }
-
-        return true;
-    }
-
     /// <summary>
     ///     Renames a file in the history, replacing the old entry with the new one.
     /// </summary>
@@ -293,13 +338,13 @@ public static class FileHistory
             }
 
             var entry = GetEntryByString(oldName);
-            if (string.IsNullOrWhiteSpace(entry) || !Entries.Contains(entry))
+            if (string.IsNullOrWhiteSpace(entry?.Path) || Entries.All(x => x.Path != entry?.Path))
             {
                 return;
             }
 
-            var index = Entries.IndexOf(entry);
-            Entries[index] = newName;
+            var index = Entries.Where(x => x.Path == entry.Path).Select(x => Entries.IndexOf(x)).First();
+            Entries[index].Path = newName;
         }
         catch (Exception e)
         {
@@ -321,15 +366,23 @@ public static class FileHistory
                 return;
             }
 
-            // Create directory if it doesn't exist.
-            var directory = Path.GetDirectoryName(_fileLocation);
-            if (directory != null && !Directory.Exists(directory))
-            {
-                Directory.CreateDirectory(directory);
-            }
+            // Create a new sorted list with pinned entries first (max 5), then unpinned entries (max MaxHistoryEntries)
+            var sortedEntries = new List<Entry>();
+
+            // Add all pinned entries first (preserving their original order) - should be max 5
+            sortedEntries.AddRange(Entries.Where(e => e.IsPinned));
+
+            // Then add all unpinned entries (preserving their original order) - limited by MaxHistoryEntries
+            sortedEntries.AddRange(Entries.Where(e => !e.IsPinned));
 
-            // Write all entries to file.
-            File.WriteAllLines(_fileLocation, Entries);
+            var historyEntries = new FileHistoryEntries
+            {
+                Entries = sortedEntries,
+                IsSortingDescending = IsSortingDescending
+            };
+            var json = JsonSerializer.Serialize(historyEntries, typeof(FileHistoryEntries),
+                FileHistoryGenerationContext.Default);
+            File.WriteAllText(_fileLocation, json);
         }
         catch (Exception ex)
         {
@@ -351,13 +404,20 @@ public static class FileHistory
                 return;
             }
 
-            var lines = File.ReadAllLines(_fileLocation);
-            foreach (var line in lines)
+            var jsonString = File.ReadAllText(_fileLocation);
+
+            if (JsonSerializer.Deserialize(
+                    jsonString, typeof(FileHistoryEntries),
+                    FileHistoryGenerationContext.Default) is not FileHistoryEntries entries)
             {
-                if (!string.IsNullOrWhiteSpace(line) && Entries.Count < MaxHistoryEntries)
-                {
-                    Entries.Add(line);
-                }
+                throw new JsonException("Failed to deserialize settings");
+            }
+
+            IsSortingDescending = entries.IsSortingDescending;
+
+            foreach (var entry in entries.Entries)
+            {
+                Entries.Add(entry);
             }
         }
         catch (Exception ex)

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

@@ -56,6 +56,7 @@ public class LanguageModel
     public string? ChangeKeybindingTooltip { get; set; }
     public string? ChangingThemeRequiresRestart { get; set; }
     public string? CheckForUpdates { get; set; }
+    public string? Clear { get; set; }
     public string? ClearEffects { get; set; }
     public string? ClipboardImage { get; set; }
     public string? Close { get; set; }
@@ -242,6 +243,7 @@ public class LanguageModel
     public string? OldMovie { get; set; }
     public string? Open { get; set; }
     public string? OpenFileDialog { get; set; }
+    public string? OpenFileHistory { get; set; }
     public string? OpenInSameWindow { get; set; }
     public string? OpenLastFile { get; set; }
     public string? OpenWith { get; set; }
@@ -256,6 +258,8 @@ public class LanguageModel
     public string? PercentComplete { get; set; }
     public string? PermanentlyDelete { get; set; }
     public string? PhotometricInterpretation { get; set; }
+    public string? Pin { get; set; }
+    public string? Pinned { get; set; }
     public string? Pixels { get; set; }
     public string? Portrait { get; set; }
     public string? Posterize { get; set; }
@@ -373,6 +377,7 @@ public class LanguageModel
     public string? Uniform { get; set; }
     public string? UniformToFill { get; set; }
     public string? Unknown { get; set; }
+    public string? Unpin { get; set; }
     public string? UnselectAll { get; set; }
     public string? UnsupportedFile { get; set; }
     public string? Up { get; set; }

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

@@ -24,7 +24,7 @@
     <PlatformTarget>ARM64</PlatformTarget>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Magick.NET-Q8-arm64" Version="14.3.0" />
+    <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="14.3.0" />
     <PackageReference Include="ReactiveUI" Version="20.2.45" />
     <PackageReference Include="SharpCompress" Version="0.39.0" />
     <PackageReference Include="ZString" Version="2.6.0" />

+ 35 - 0
src/PicView.Core/ViewModels/TranslationViewModel.cs

@@ -277,10 +277,45 @@ public class TranslationViewModel : ReactiveObject
         MoveToRecycleBin = TranslationManager.Translation.MoveToRecycleBin;
         ShowConfirmationDialogWhenPermanentlyDeletingFile = TranslationManager.Translation.ShowConfirmationDialogWhenPermanentlyDeletingFile;
         Downloading = TranslationManager.Translation.Downloading;
+        Pinned = TranslationManager.Translation.Pinned;
+        Unpin = TranslationManager.Translation.Unpin;
+        Pin = TranslationManager.Translation.Pin;
+        Clear = TranslationManager.Translation.Clear;
+        OpenFileHistory = TranslationManager.Translation.OpenFileHistory;
     }
 
     #region Static Translation Strings
     
+    public string? Pinned
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    
+    public string? Unpin
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    
+    public string? Pin
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    
+    public string? Clear
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    
+    public string? OpenFileHistory
+    {
+        get;
+        set => this.RaiseAndSetIfChanged(ref field, value);
+    }
+    
     public string? Downloading
     {
         get;

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác