Bläddra i källkod

Add file search functionality with enhanced UI and support for localized strings #36

- Introduced `FileSearcher` in `PicView.Core` to calculate relevance scores and return filtered file results.
- Added `FileSearchDialog` to provide a popup-based search interface with debounce support and focus navigation.
- Integrated search command into menus and keybindings (`Ctrl+F` for Windows, `Cmd+F` for macOS).
- Updated translations for new "Search" and related terms across supported languages.
- Included new custom control `HighlightableTextBlock` for result highlighting.
- Refined styles and layouts for consistency with existing framework.
Ruben 1 vecka sedan
förälder
incheckning
3d10a89905
48 ändrade filer med 935 tillägg och 304 borttagningar
  1. 3 0
      src/PicView.Avalonia.Win32/PicView.Avalonia.Win32.csproj
  2. 5 2
      src/PicView.Avalonia/CustomControls/AnimatedPopUp.cs
  3. 207 0
      src/PicView.Avalonia/CustomControls/HighlightableTextBlock.cs
  4. 12 7
      src/PicView.Avalonia/Functions/FunctionsMapper.cs
  5. 1 1
      src/PicView.Avalonia/Navigation/ImageIterator.cs
  6. 7 0
      src/PicView.Avalonia/PicView.Avalonia.csproj
  7. 1 0
      src/PicView.Avalonia/PicViewTheme/AllControls.axaml
  8. 14 2
      src/PicView.Avalonia/PicViewTheme/Controls/AutoCompleteBox.axaml
  9. 6 0
      src/PicView.Avalonia/PicViewTheme/Controls/Button.axaml
  10. 1 2
      src/PicView.Avalonia/PicViewTheme/Controls/ListBoxItem.axaml
  11. 1 0
      src/PicView.Avalonia/PicViewTheme/ResourceDictionaries/MainColors.axaml
  12. 11 0
      src/PicView.Avalonia/UI/DialogManager.cs
  13. 6 0
      src/PicView.Avalonia/ViewModels/NavigationViewModel.cs
  14. 2 0
      src/PicView.Avalonia/ViewModels/ToolsViewModel.cs
  15. 1 0
      src/PicView.Avalonia/Views/Gallery/GalleryView.axaml
  16. 26 0
      src/PicView.Avalonia/Views/Main/KeybindingsView.axaml
  17. 11 1
      src/PicView.Avalonia/Views/UC/HoverBar.axaml.cs
  18. 5 1
      src/PicView.Avalonia/Views/UC/Menus/ImageMenu.axaml
  19. 68 0
      src/PicView.Avalonia/Views/UC/PopUps/FileSearchDialog.axaml
  20. 152 0
      src/PicView.Avalonia/Views/UC/PopUps/FileSearchDialog.axaml.cs
  21. 1 0
      src/PicView.Core.MacOS/MacOsKeybindings.cs
  22. 2 1
      src/PicView.Core.WindowsNT/WindowsKeybindings.cs
  23. 1 0
      src/PicView.Core/Config/Languages/da.json
  24. 1 0
      src/PicView.Core/Config/Languages/de.json
  25. 10 10
      src/PicView.Core/Config/Languages/en.json
  26. 1 0
      src/PicView.Core/Config/Languages/es.json
  27. 1 0
      src/PicView.Core/Config/Languages/fr.json
  28. 1 0
      src/PicView.Core/Config/Languages/he.json
  29. 1 0
      src/PicView.Core/Config/Languages/hu.json
  30. 1 0
      src/PicView.Core/Config/Languages/it.json
  31. 1 0
      src/PicView.Core/Config/Languages/ja.json
  32. 1 0
      src/PicView.Core/Config/Languages/ko.json
  33. 1 0
      src/PicView.Core/Config/Languages/nl.json
  34. 1 0
      src/PicView.Core/Config/Languages/pl.json
  35. 1 0
      src/PicView.Core/Config/Languages/pt-br.json
  36. 1 0
      src/PicView.Core/Config/Languages/ro.json
  37. 1 0
      src/PicView.Core/Config/Languages/ru.json
  38. 1 0
      src/PicView.Core/Config/Languages/sr.json
  39. 1 0
      src/PicView.Core/Config/Languages/sv.json
  40. 1 0
      src/PicView.Core/Config/Languages/tr.json
  41. 1 0
      src/PicView.Core/Config/Languages/zh-CN.json
  42. 1 0
      src/PicView.Core/Config/Languages/zh-TW.json
  43. 9 0
      src/PicView.Core/FileSearch/FileSearchResult.cs
  44. 72 0
      src/PicView.Core/FileSearch/FileSearcher.cs
  45. 1 0
      src/PicView.Core/Localization/LanguageModel.cs
  46. 3 3
      src/PicView.Core/Preloading/Preloader.cs
  47. 5 1
      src/PicView.Core/ViewModels/PicViewerModel.cs
  48. 273 273
      src/PicView.Core/ViewModels/TranslationViewModel.cs

+ 3 - 0
src/PicView.Avalonia.Win32/PicView.Avalonia.Win32.csproj

@@ -84,6 +84,9 @@
       <DependentUpon>EffectsWindow.axaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
+      <Compile Update="Views\PrintPreviewWindow.axaml.cs">
+          <DependentUpon>PrintPreviewWindow.axaml</DependentUpon>
+      </Compile>
   </ItemGroup>
 
 	<ItemGroup>

+ 5 - 2
src/PicView.Avalonia/CustomControls/AnimatedPopUp.cs

@@ -122,9 +122,12 @@ public class AnimatedPopUp : ContentControl
         }
     }
 
-    // ReSharper disable once UnusedParameter.Global
+    // ReSharper disable once UnusedMember.Global
     public void KeyDownHandler(object? sender, KeyEventArgs e)
     {
-        RaiseEvent(e);
+        if (e.Key is Key.Escape)
+        {
+            _ = AnimatedClosing();
+        }
     }
 }

+ 207 - 0
src/PicView.Avalonia/CustomControls/HighlightableTextBlock.cs

@@ -0,0 +1,207 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace PicView.Avalonia.CustomControls;
+
+public class HighlightableTextBlock : Control
+{
+    protected override Type StyleKeyOverride => typeof(TextBlock);
+
+    /// <summary>
+    /// Defines the Text property.
+    /// </summary>
+    public static readonly StyledProperty<string?> TextProperty =
+        TextBlock.TextProperty.AddOwner<HighlightableTextBlock>();
+
+    /// <summary>
+    /// Defines the Foreground property.
+    /// </summary>
+    public static readonly StyledProperty<IBrush?> ForegroundProperty =
+        TextBlock.ForegroundProperty.AddOwner<HighlightableTextBlock>();
+
+    /// <summary>
+    /// Defines the HighlightBrush property for the highlighted text section.
+    /// </summary>
+    public static readonly StyledProperty<IBrush?> HighlightBrushProperty =
+        AvaloniaProperty.Register<HighlightableTextBlock, IBrush?>(nameof(HighlightBrush));
+
+    /// <summary>
+    /// Defines the start index of the highlight.
+    /// </summary>
+    public static readonly StyledProperty<int> HighlightStartProperty =
+        AvaloniaProperty.Register<HighlightableTextBlock, int>(nameof(HighlightStart), -1);
+
+    /// <summary>
+    /// Defines the end index of the highlight.
+    /// </summary>
+    public static readonly StyledProperty<int> HighlightEndProperty =
+        AvaloniaProperty.Register<HighlightableTextBlock, int>(nameof(HighlightEnd), -1);
+
+    // Add other text properties for a more complete control
+    public static readonly StyledProperty<FontFamily> FontFamilyProperty =
+        TextBlock.FontFamilyProperty.AddOwner<HighlightableTextBlock>();
+
+    public static readonly StyledProperty<double> FontSizeProperty =
+        TextBlock.FontSizeProperty.AddOwner<HighlightableTextBlock>();
+
+    public static readonly StyledProperty<FontStyle> FontStyleProperty =
+        TextBlock.FontStyleProperty.AddOwner<HighlightableTextBlock>();
+
+    public static readonly StyledProperty<FontWeight> FontWeightProperty =
+        TextBlock.FontWeightProperty.AddOwner<HighlightableTextBlock>();
+
+    public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
+        TextBlock.TextWrappingProperty.AddOwner<HighlightableTextBlock>();
+
+    // Static constructor to register property change handlers
+    static HighlightableTextBlock()
+    {
+        // Any property change that affects the visual appearance should trigger a redraw.
+        AffectsRender<HighlightableTextBlock>(
+            TextProperty,
+            ForegroundProperty,
+            HighlightBrushProperty,
+            HighlightStartProperty,
+            HighlightEndProperty,
+            FontFamilyProperty,
+            FontSizeProperty,
+            FontStyleProperty,
+            FontWeightProperty);
+
+        // Properties that affect the size of the control should trigger a remeasure.
+        AffectsMeasure<HighlightableTextBlock>(
+            TextProperty,
+            FontSizeProperty,
+            FontFamilyProperty,
+            FontWeightProperty,
+            FontStyleProperty,
+            TextWrappingProperty);
+    }
+
+    // CLR Property Wrappers
+    public string? Text
+    {
+        get => GetValue(TextProperty);
+        set => SetValue(TextProperty, value);
+    }
+
+    public IBrush? Foreground
+    {
+        get => GetValue(ForegroundProperty);
+        set => SetValue(ForegroundProperty, value);
+    }
+
+    public IBrush? HighlightBrush
+    {
+        get => GetValue(HighlightBrushProperty);
+        set => SetValue(HighlightBrushProperty, value);
+    }
+
+    public int HighlightStart
+    {
+        get => GetValue(HighlightStartProperty);
+        set => SetValue(HighlightStartProperty, value);
+    }
+
+    public int HighlightEnd
+    {
+        get => GetValue(HighlightEndProperty);
+        set => SetValue(HighlightEndProperty, value);
+    }
+
+    public FontFamily FontFamily
+    {
+        get => GetValue(FontFamilyProperty);
+        set => SetValue(FontFamilyProperty, value);
+    }
+
+    public double FontSize
+    {
+        get => GetValue(FontSizeProperty);
+        set => SetValue(FontSizeProperty, value);
+    }
+
+    public FontStyle FontStyle
+    {
+        get => GetValue(FontStyleProperty);
+        set => SetValue(FontStyleProperty, value);
+    }
+
+    public FontWeight FontWeight
+    {
+        get => GetValue(FontWeightProperty);
+        set => SetValue(FontWeightProperty, value);
+    }
+
+    public TextWrapping TextWrapping
+    {
+        get => GetValue(TextWrappingProperty);
+        set => SetValue(TextWrappingProperty, value);
+    }
+
+
+    /// <summary>
+    /// Renders the control.
+    /// </summary>
+    public override void Render(DrawingContext context)
+    {
+        var text = Text;
+        if (string.IsNullOrEmpty(text))
+        {
+            return;
+        }
+
+        // Create the FormattedText object
+        var formattedText = new FormattedText(
+            text,
+            CultureInfo.CurrentCulture,
+            CultureInfo.CurrentCulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight,
+            new Typeface(FontFamily, FontStyle, FontWeight),
+            FontSize,
+            Foreground);
+
+        // 1. Apply the default foreground color to the entire text
+        if (Foreground != null)
+        {
+            formattedText.SetForegroundBrush(Foreground);
+        }
+
+        // 2. Apply the highlight brush if the range and brush are valid
+        var highlightBrush = HighlightBrush;
+        var start = HighlightStart;
+        var end = HighlightEnd;
+
+        if (highlightBrush != null && start >= 0 && end > start && end <= text.Length)
+        {
+            var length = end - start;
+            formattedText.SetForegroundBrush(highlightBrush, start, length);
+        }
+
+        // Draw the formatted text to the screen
+        context.DrawText(formattedText, new Point(0, 0));
+    }
+
+    /// <summary>
+    /// Measures the desired size of the control.
+    /// </summary>
+    protected override Size MeasureOverride(Size availableSize)
+    {
+        var text = Text;
+        if (string.IsNullOrEmpty(text))
+        {
+            return new Size();
+        }
+
+        var formattedText = new FormattedText(
+            text,
+            CultureInfo.CurrentCulture,
+            CultureInfo.CurrentCulture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight,
+            new Typeface(FontFamily, FontStyle, FontWeight),
+            FontSize,
+            Foreground);
+
+        return new Size(formattedText.Width, formattedText.Height);
+    }
+}

+ 12 - 7
src/PicView.Avalonia/Functions/FunctionsMapper.cs

@@ -50,6 +50,8 @@ public static class FunctionsMapper
             
             "Next100" => Next100,
             "Prev100" => Prev100,
+
+            "Search" => Search,
             
             // Rotate
             "RotateLeft" => RotateLeft,
@@ -271,6 +273,9 @@ public static class FunctionsMapper
     /// <inheritdoc cref="NavigationManager.Prev100(MainViewModel)" />
     public static async ValueTask Prev100() =>
         await NavigationManager.Prev100(Vm).ConfigureAwait(false);
+
+    public static async ValueTask Search() =>
+        await Dispatcher.UIThread.InvokeAsync(DialogManager.AddFileSearchDialog);
     
 
     /// <inheritdoc cref="RotationNaRotationNavigationp(MainViewModel)" />
@@ -654,31 +659,31 @@ public static class FunctionsMapper
 
     #region Sorting
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesByName() =>
         await FileListManager.UpdateFileList(Vm.PlatformService, Vm, SortFilesBy.Name).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesByCreationTime() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.CreationTime).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesByLastAccessTime() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.LastAccessTime).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesByLastWriteTime() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.LastWriteTime).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesBySize() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.FileSize).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesByExtension() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.Extension).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, SortFilesBy)" />
     public static async ValueTask SortFilesRandomly() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.Random).ConfigureAwait(false);
 

+ 1 - 1
src/PicView.Avalonia/Navigation/ImageIterator.cs

@@ -693,7 +693,7 @@ public class ImageIterator : IAsyncDisposable
         await UpdateImage.UpdateSourceSlim(_vm, index, imageSource, width, height, ImagePaths, token);
     }
 
-    public async Task SlimUpdate(int index, object? imageSource)
+    public async ValueTask SlimUpdate(int index, object? imageSource)
     {
         var magickImage = GetImage.CreateAndPingMagickImage(ImagePaths[index]);
         var imageModel = new ImageModel

+ 7 - 0
src/PicView.Avalonia/PicView.Avalonia.csproj

@@ -122,6 +122,13 @@
       <Compile Update="Views\Main\PrintPreviewView.axaml.cs">
           <DependentUpon>PrintPreviewView.axaml</DependentUpon>
       </Compile>
+      <Compile Update="Views\UC\PopUps\FileSearchDialog.axaml.cs">
+          <DependentUpon>FileSearchDialog.axaml</DependentUpon>
+          <SubType>Code</SubType>
+      </Compile>
+      <Compile Update="CustomControls\AnimatedPopUp.cs">
+          <DependentUpon>AnimatedMenu.cs</DependentUpon>
+      </Compile>
   </ItemGroup>
 
 

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

@@ -28,6 +28,7 @@
                 <ResourceInclude Source="Controls/Expander.axaml" />
                 <ResourceInclude Source="Controls/FlyoutPresenter.axaml" />
                 <ResourceInclude Source="Controls/HyperlinkButton.axaml" />
+                <ResourceInclude Source="Controls/ItemsControl.axaml" />
                 <ResourceInclude Source="Controls/ListBox.axaml" />
                 <ResourceInclude Source="Controls/ListBoxItem.axaml" />
                 <ResourceInclude Source="Controls/MainScrollbar.axaml" />

+ 14 - 2
src/PicView.Avalonia/PicViewTheme/Controls/AutoCompleteBox.axaml

@@ -31,12 +31,24 @@
                         PlacementTarget="{TemplateBinding}">
                         <ListBox
                             Background="{TemplateBinding Background}"
-                            BorderThickness="0"
+                            BorderThickness="1"
                             Foreground="{TemplateBinding Foreground}"
                             ItemTemplate="{TemplateBinding ItemTemplate}"
                             Name="PART_SelectingItemsControl"
                             ScrollViewer.HorizontalScrollBarVisibility="Auto"
-                            ScrollViewer.VerticalScrollBarVisibility="Auto" />
+                            ScrollViewer.VerticalScrollBarVisibility="Auto"
+                            Width="{TemplateBinding Width}">
+                            <ListBox.ItemsPanel>
+                                <ItemsPanelTemplate>
+                                    <VirtualizingStackPanel HorizontalAlignment="Left" VerticalAlignment="Center" />
+                                </ItemsPanelTemplate>
+                            </ListBox.ItemsPanel>
+                            <ListBox.Styles>
+                                <Style Selector="ListBoxItem">
+                                    <Setter Property="Padding" Value="7" />
+                                </Style>
+                            </ListBox.Styles>
+                        </ListBox>
                     </Popup>
                 </Panel>
             </ControlTemplate>

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

@@ -241,6 +241,12 @@
             <Setter Property="Background" Value="{DynamicResource AltBackgroundHoverColor}" />
             <Setter Property="BorderBrush" Value="{DynamicResource SecondaryBorderColor}" />
         </Style>
+
+        <Style Selector="^.focus:focus-within /template/ ContentPresenter#PART_ContentPresenter">
+            <Setter Property="BorderBrush" Value="{DynamicResource AccentColor}" />
+            <Setter Property="BorderThickness" Value="1" />
+        </Style>
+
         <Style Selector="^:disabled">
             <Setter Property="Opacity" Value="{StaticResource ThemeDisabledOpacity}" />
         </Style>

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

@@ -5,8 +5,7 @@
     <ControlTheme TargetType="ListBoxItem" x:Key="{x:Type ListBoxItem}">
         <Setter Property="Background" Value="Transparent" />
         <Setter Property="BorderBrush" Value="Transparent" />
-        <Setter Property="BorderThickness" Value="3" />
-        <Setter Property="Margin" Value="20" />
+        <Setter Property="BorderThickness" Value="1" />
         <Setter Property="Template">
             <ControlTemplate>
                 <ContentPresenter

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

@@ -7,6 +7,7 @@
         <ResourceDictionary x:Key="Default">
             <Color x:Key="MainTextColor">#E2E0E0</Color>
             <Color x:Key="MainTextColorFaded">#d6d4d4</Color>
+            <SolidColorBrush Color="{StaticResource MainTextColorFaded}" x:Key="MainTextColorFadedBrush" />
 
             <!--  Used for icons that should stay white in both themes  -->
             <Color x:Key="SecondaryTextColor">#fff</Color>

+ 11 - 0
src/PicView.Avalonia/UI/DialogManager.cs

@@ -68,4 +68,15 @@ public static class DialogManager
             desktop.MainWindow?.Close();
         }
     }
+
+    public static void AddFileSearchDialog()
+    {
+        if (UIHelper.GetMainView.MainGrid.Children.OfType<FileSearchDialog>().Any())
+        {
+            return;
+        }
+
+        MenuManager.CloseMenus(UIHelper.GetMainView.DataContext as MainViewModel);
+        UIHelper.GetMainView.MainGrid.Children.Add(new FileSearchDialog());
+    }
 }

+ 6 - 0
src/PicView.Avalonia/ViewModels/NavigationViewModel.cs

@@ -60,6 +60,12 @@ public class NavigationViewModel : IDisposable
     {
         await NavigationManager.NavigateIncrements(next: false, false, true).ConfigureAwait(false);
     });
+
+    public ReactiveCommand<string> LoadFileFromStringCommand { get; } = new(async (value, _) =>
+    {
+        await NavigationManager.LoadPicFromFile(value, UIHelper.GetMainView.DataContext as MainViewModel)
+            .ConfigureAwait(false);
+    });
     
     public void Dispose()
     {

+ 2 - 0
src/PicView.Avalonia/ViewModels/ToolsViewModel.cs

@@ -254,6 +254,8 @@ public class ToolsViewModel : IDisposable
         await FunctionsMapper.ChangeBackground();
     });
 
+    public ReactiveCommand ShowSearchCommand { get; } = new(async (_, _) => { await FunctionsMapper.Search(); });
+
     public ReactiveCommand ShowSideBySideCommand { get; } =
         new(async (_, _) => { await FunctionsMapper.SideBySide(); });
 

+ 1 - 0
src/PicView.Avalonia/Views/Gallery/GalleryView.axaml

@@ -168,6 +168,7 @@
             <ListBox.Styles>
                 <Style Selector="ListBoxItem">
                     <Setter Property="Margin" Value="{CompiledBinding Gallery.GalleryItem.ItemMargin.Value}" />
+                    <Setter Property="BorderThickness" Value="3" />
                 </Style>
             </ListBox.Styles>
         </customControls:GalleryListBox>

+ 26 - 0
src/PicView.Avalonia/Views/Main/KeybindingsView.axaml

@@ -238,6 +238,7 @@
                 Width="140" />
         </StackPanel>
 
+        <!--  Next folder  -->
         <StackPanel Margin="15,0,10,10" Orientation="Horizontal">
             <TextBlock
                 Classes="txt"
@@ -261,6 +262,7 @@
                 Width="140" />
         </StackPanel>
 
+        <!--  Previous folder  -->
         <StackPanel Margin="15,0,10,10" Orientation="Horizontal">
             <TextBlock
                 Classes="txt"
@@ -283,6 +285,30 @@
                 Width="140" />
         </StackPanel>
 
+        <!--  Search  -->
+        <StackPanel Margin="15,5,10,10" Orientation="Horizontal">
+            <TextBlock
+                Classes="txt"
+                FontFamily="/Assets/Fonts/Roboto-Bold.ttf#Roboto"
+                Text="{CompiledBinding Translation.Search.Value,
+                                       Mode=OneWay}"
+                Width="170" />
+            <customControls:KeybindTextBox
+                Classes="hover TStyle"
+                MethodName="Search"
+                ToolTip.Tip="{CompiledBinding Translation.Search.Value,
+                                              Mode=OneWay}"
+                Width="140" />
+            <customControls:KeybindTextBox
+                Alt="True"
+                Classes="hover TStyle"
+                MethodName="Search"
+                ToolTip.Tip="{CompiledBinding Translation.Search.Value,
+                                              Mode=OneWay}"
+                Width="140" />
+        </StackPanel>
+
+        <!--  Select gallery thumb  -->
         <StackPanel Margin="15,5,10,10" Orientation="Horizontal">
             <TextBlock
                 Classes="txt"

+ 11 - 1
src/PicView.Avalonia/Views/UC/HoverBar.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
+using Avalonia.Threading;
 using PicView.Avalonia.Functions;
 using PicView.Avalonia.Navigation;
 using PicView.Avalonia.UI;
@@ -174,7 +175,13 @@ public partial class HoverBar : UserControl
         {
             if (props.IsRightButtonPressed)
             {
-                //TODO: Create popup window to navigate to index
+                ShowSearchDialog();
+
+                // Wait for animation to finish to properly close tooltip
+                await Task.Delay(TimeSpan.FromSeconds(0.3));
+                Dispatcher.UIThread.Post(() => { ToolTip.SetIsOpen(ProgressBar, false); },
+                    DispatcherPriority.Background);
+                
             }
         }
         else
@@ -195,6 +202,9 @@ public partial class HoverBar : UserControl
     private static void ShowQuickEditingDialog() =>
         UIHelper.GetMainView.MainGrid.Children.Add(new QuickEditingDialog());
 
+    private static void ShowSearchDialog() =>
+        UIHelper.GetMainView.MainGrid.Children.Add(new FileSearchDialog());
+
     private static void ShowMainContextMenu()
     {
         if (UIHelper.GetMainView.Resources.TryGetResource("MainContextMenu", Application.Current.ActualThemeVariant, out var value)

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

@@ -68,12 +68,16 @@
                     Canvas.Top="-1"
                     Classes="hover"
                     ClickMode="Release"
+                    Command="{CompiledBinding Tools.ShowSearchCommand}"
                     Data="{StaticResource SearchGeometry}"
                     Foreground="{DynamicResource MainTextColor}"
                     Height="47"
                     IconHeight="16"
                     IconWidth="16"
-                    IsEnabled="False"
+                    IsEnabled="{CompiledBinding PicViewer.FileInfo.Value,
+                                                Converter={x:Static ObjectConverters.IsNotNull}}"
+                    ToolTip.Tip="{CompiledBinding Translation.Search.Value,
+                                                  Mode=OneWay}"
                     Width="45" />
 
                 <customControls:IconButton

+ 68 - 0
src/PicView.Avalonia/Views/UC/PopUps/FileSearchDialog.axaml

@@ -0,0 +1,68 @@
+<customControls:AnimatedPopUp
+    ClickingOutsideCloses="True"
+    Padding="10"
+    x:Class="PicView.Avalonia.Views.UC.PopUps.FileSearchDialog"
+    x:DataType="viewModels:MainViewModel"
+    xmlns="https://github.com/avaloniaui"
+    xmlns:customControls="clr-namespace:PicView.Avalonia.CustomControls"
+    xmlns:viewModels="clr-namespace:PicView.Avalonia.ViewModels"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <StackPanel Background="Transparent" Width="425">
+
+        <TextBox
+            Background="{DynamicResource AltBackgroundColor}"
+            BorderBrush="{DynamicResource MainBorderColor}"
+            BorderThickness="1"
+            CornerRadius="3"
+            FontFamily="/Assets/Fonts/Roboto-Regular.ttf#Roboto"
+            Foreground="{DynamicResource MainTextColor}"
+            Margin="0,0,0,5"
+            Padding="5,6,0,6"
+            VerticalAlignment="Center"
+            Watermark="{CompiledBinding PicViewer.FileInfo.Value,
+                                        Mode=OneWay}"
+            Width="425"
+            x:Name="SearchBox" />
+
+        <Border
+            BorderBrush="{DynamicResource MainBorderColor}"
+            BorderThickness="0,1,1,1"
+            CornerRadius="6"
+            IsVisible="{Binding !!PicViewer.FilteredFileInfos.Value.Count}">
+            <customControls:AutoScrollViewer MaxHeight="400">
+                <Border
+                    Background="#10000000"
+                    Classes="borderStyle"
+                    CornerRadius="6">
+                    <ItemsControl ItemsSource="{CompiledBinding PicViewer.FilteredFileInfos.Value}">
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate>
+                                <Button
+                                    Background="Transparent"
+                                    BorderBrush="{DynamicResource MainBorderColor}"
+                                    BorderThickness="0,0,0,1"
+                                    Classes="noBorderHover focus"
+                                    Command="{Binding #SearchBox.((viewModels:MainViewModel)DataContext).Navigation.LoadFileFromStringCommand}"
+                                    CommandParameter="{Binding File.FullName}"
+                                    Focusable="True"
+                                    IsTabStop="True"
+                                    Padding="5,10">
+                                    <customControls:HighlightableTextBlock
+                                        Classes="txt"
+                                        HighlightBrush="{DynamicResource AccentColor}"
+                                        HighlightEnd="{Binding MatchEndIndex}"
+                                        HighlightStart="{Binding MatchStartIndex}"
+                                        IsHitTestVisible="False"
+                                        Margin="2,0,0,0"
+                                        Text="{Binding File.Name}"
+                                        TextWrapping="Wrap" />
+                                </Button>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                    </ItemsControl>
+                </Border>
+            </customControls:AutoScrollViewer>
+        </Border>
+
+    </StackPanel>
+</customControls:AnimatedPopUp>

+ 152 - 0
src/PicView.Avalonia/Views/UC/PopUps/FileSearchDialog.axaml.cs

@@ -0,0 +1,152 @@
+using System.Collections.ObjectModel;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+using PicView.Avalonia.CustomControls;
+using PicView.Avalonia.Navigation;
+using PicView.Avalonia.UI;
+using PicView.Avalonia.ViewModels;
+using PicView.Core.FileSearch;
+using R3;
+
+namespace PicView.Avalonia.Views.UC.PopUps;
+
+public partial class FileSearchDialog : AnimatedPopUp
+{
+    private readonly CompositeDisposable _disposables = new();
+
+    public FileSearchDialog()
+    {
+        DataContext = UIHelper.GetMainView.DataContext as MainViewModel;
+        if (DataContext is MainViewModel { PicViewer.FilteredFileInfos.CurrentValue: null } vm)
+        {
+            vm.PicViewer.FilteredFileInfos.Value = [];
+        }
+
+        InitializeComponent();
+        Loaded += OnLoaded;
+    }
+
+    private void OnLoaded(object? sender, RoutedEventArgs e)
+    {
+        // Ensure we don't double-subscribe if Loaded fires multiple times
+        _disposables.Clear();
+        SetupSearchSubscription();
+
+        SearchBox.Focus();
+
+        KeyDown += OnKeyDown;
+    }
+
+    private void OnKeyDown(object? sender, KeyEventArgs e)
+    {
+        switch (e.Key)
+        {
+            case Key.Down:
+                MoveFocus(NavigationDirection.Next);
+                e.Handled = true;
+                break;
+            case Key.Up:
+                MoveFocus(NavigationDirection.Previous);
+                e.Handled = true;
+                break;
+        }
+    }
+
+    private void MoveFocus(NavigationDirection direction)
+    {
+        if (TopLevel.GetTopLevel(this) is not { FocusManager: { } focusManager })
+        {
+            return;
+        }
+
+        var focused = focusManager.GetFocusedElement();
+        if (focused is null)
+        {
+            return;
+        }
+
+        var next = KeyboardNavigationHandler.GetNext(focused, direction);
+        next?.Focus();
+    }
+
+    protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromLogicalTree(e);
+        Loaded -= OnLoaded;
+        Dispose();
+    }
+
+    private void SetupSearchSubscription()
+    {
+        if (DataContext is not MainViewModel vm)
+        {
+            return;
+        }
+
+        // Create an observable that emits a value whenever SearchQuery changes.
+        Observable.EveryValueChanged(SearchBox, x => x.Text)
+            .Skip(1)
+            // Wait for X ms of inactivity before processing to avoid searching on every keystroke.
+            .Debounce(TimeSpan.FromMilliseconds(50))
+            // Subscribe to the results and update the UI collection.
+            .SubscribeAwait(async (text, ct) =>
+            {
+                if (string.IsNullOrWhiteSpace(text))
+                {
+                    vm.PicViewer.FilteredFileInfos.Value?.Clear();
+                    return;
+                }
+
+                const int batchSize = 25;
+
+                IEnumerable<FileSearchResult>? results = null;
+                await Task.Run(
+                    () => { results = FileSearcher.GetFileSearchResults(NavigationManager.GetCollection, text); }, ct);
+
+                var fileSearchResults = results as FileSearchResult[] ?? results.ToArray();
+                vm.PicViewer.FilteredFileInfos.Value =
+                    new ObservableCollection<FileSearchResult>(fileSearchResults.Take(batchSize));
+                if (fileSearchResults.Length < batchSize)
+                {
+                    return;
+                }
+
+                await Task.Delay(100, ct);
+
+                for (var i = batchSize; i < fileSearchResults.Length; i += batchSize)
+                {
+                    var batch = fileSearchResults.Skip(i).Take(batchSize);
+                    foreach (var item in batch)
+                    {
+                        await Task.Delay(100, ct);
+                        ct.ThrowIfCancellationRequested();
+                        if (!ct.IsCancellationRequested)
+                        {
+                            vm.PicViewer.FilteredFileInfos.Value.Add(item);
+                        }
+                    }
+                }
+            }, AwaitOperation.Switch, false)
+            // Add the subscription to our disposable manager for cleanup.
+            .AddTo(_disposables);
+
+        // Close when changing picture
+        Observable.EveryValueChanged(vm.PicViewer, x => x.FileInfo.CurrentValue)
+            .Skip(1)
+            .SubscribeAwait(async (_, _) => { await AnimatedClosing(); })
+            .AddTo(_disposables);
+    }
+
+    public void Dispose()
+    {
+        _disposables.Dispose();
+        KeyDown -= OnKeyDown;
+
+        if (DataContext is MainViewModel vm)
+        {
+            vm.PicViewer.FilteredFileInfos.Value.Clear();
+        }
+    }
+}

+ 1 - 0
src/PicView.Core.MacOS/MacOsKeybindings.cs

@@ -66,6 +66,7 @@ public static class MacOsKeybindings
                                                 "Cmd+N": "NewWindow",
                                                 "J": "ResizeWindow",
                                                 "Alt+Cmd+I": "ResizeWindow",
+                                                "Cmd+F": "Search",
                                               }
                                               """;
 }

+ 2 - 1
src/PicView.Core.WindowsNT/WindowsKeybindings.cs

@@ -73,7 +73,8 @@ public static class WindowsKeybindings
                                                "OemMinus": "ZoomOut",
                                                "Delete": "DeleteFile",
                                                "Shift+Delete": "DeleteFilePermanently",
-                                               "Alt+Enter": "Fullscreen"
+                                               "Alt+Enter": "Fullscreen",
+                                               "Ctrl+F": "Search",
                                              }
                                              """;
 }

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

@@ -316,6 +316,7 @@
   "Scrolling": "Scrolling",
   "ScrollingDisabled": "Scrolling slået fra",
   "ScrollingEnabled": "Scrolling slået til",
+  "Search": "Søg",
   "SearchSubdirectory": "Tilføj undermapper til filsøgningen",
   "SecAbbreviation": "Sek.",
   "SelectAll": "Vælg alle",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Scrollen",
   "ScrollingDisabled": "Scrollen deaktiviert",
   "ScrollingEnabled": "Scrollen aktiviert",
+  "Search": "Suchen",
   "SearchSubdirectory": "Unterverzeichnisse durchsuchen",
   "SecAbbreviation": "Sek.",
   "SelectAll": "Alle auswählen",

+ 10 - 10
src/PicView.Core/Config/Languages/en.json

@@ -30,6 +30,7 @@
   "BitDepth": "Bit depth",
   "BlackAndWhite": "Grayscale",
   "Blur": "Blur",
+  "Bottom": "Bottom",
   "BottomGalleryItemSize": "Size of thumbnails in the bottom gallery",
   "BottomGalleryThumbnailStretch": "Thumbnail stretch in the bottom gallery",
   "Brightness": "Brightness",
@@ -53,6 +54,7 @@
   "CloseGallery": "Close gallery",
   "CloseWindowPrompt": "Close the window?",
   "CloudyWeather": "Cloudy weather",
+  "Color": "Color",
   "ColorPickerTool": "Color Picker Tool",
   "ColorPickerToolTooltip": "Pick color from image",
   "ColorRepresentation": "Color representation",
@@ -66,6 +68,7 @@
   "ConvertedToBase64": "Converted to base64",
   "CoolWhiteFluorescent": "Cool white fluorescent",
   "CopiedImage": "Copied image to clipboard",
+  "Copies": "Copies",
   "Copy": "Copy",
   "CopyFile": "Copy file",
   "CopyImage": "Copy image",
@@ -204,6 +207,7 @@
   "Lossy": "Lossy",
   "Low": "Low",
   "Manual": "Manual",
+  "Margins": "Margins (mm)",
   "MaxAperture": "Max aperture",
   "Maximize": "Maximize",
   "MegaPixels": "megapixels",
@@ -254,6 +258,7 @@
   "Orientation": "Orientation",
   "OutputFolder": "Output folder",
   "Pan": "Pan",
+  "PaperSize": "Paper Size",
   "PasswordArchive": "Password protected archive not supported",
   "PasteImageFromClipholder": "Paste image from clipboard",
   "PencilSketch": "Pencil Sketch",
@@ -272,6 +277,7 @@
   "Print": "Print",
   "PrintSizeCm": "Print size (cm)",
   "PrintSizeIn": "Print size (in)",
+  "Printer": "Printer",
   "Quality": "Quality",
   "Random": "Random",
   "RawCamera": "Raw Camera",
@@ -299,6 +305,7 @@
   "Save": "Save",
   "SaveAs": "Save as",
   "SavingFileFailed": "Saving file failed",
+  "Scale": "Scale",
   "ScrollAndRotate": "Scroll and rotate",
   "ScrollDirection": "Scroll direction",
   "ScrollDown": "Scroll down",
@@ -309,6 +316,7 @@
   "Scrolling": "Scrolling",
   "ScrollingDisabled": "Scrolling disabled",
   "ScrollingEnabled": "Scrolling enabled",
+  "Search": "Search",
   "SearchSubdirectory": "Search subdirectories",
   "SecAbbreviation": "Sec.",
   "SelectAll": "Select All",
@@ -373,6 +381,7 @@
   "ToggleLooping": "Toggle looping",
   "ToggleScroll": "Toggle scroll",
   "ToggleTaskbarProgress": "Display taskbar progress",
+  "Top": "Top",
   "UnableToRender": "Unable to render image",
   "Unassociate": "Unassociate",
   "Uncalibrated": "Uncalibrated",
@@ -408,14 +417,5 @@
   "_2Star": "2 star rating",
   "_3Star": "3 star rating",
   "_4Star": "4 star rating",
-  "_5Star": "5 star rating",
-
-  "Printer": "Printer",
-  "PaperSize": "Paper Size",
-  "Scale": "Scale",
-  "Color": "Color",
-  "Copies": "Copies",
-  "Margins": "Margins (mm)",
-  "Top": "Top",
-  "Bottom": "Bottom"
+  "_5Star": "5 star rating"
 }

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

@@ -316,6 +316,7 @@
   "Scrolling": "Desplazar",
   "ScrollingDisabled": "Rueda del mouse desactivada",
   "ScrollingEnabled": "Rueda del mouse activada",
+  "Search": "Buscar",
   "SearchSubdirectory": "Buscar subdirectorios",
   "SecAbbreviation": "Seg.",
   "SelectAll": "Seleccionar todo",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Défilement",
   "ScrollingDisabled": "Défilement désactivé",
   "ScrollingEnabled": "Défilement activé",
+  "Search": "Rechercher",
   "SearchSubdirectory": "Chercher les sous-répertoires",
   "SecAbbreviation": "Sec.",
   "SelectAll": "Tout sélectionner",

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

@@ -316,6 +316,7 @@
   "Scrolling": "גלילה",
   "ScrollingDisabled": "גלילה מבוטלת",
   "ScrollingEnabled": "גלילה מופעלת",
+  "Search": "חיפוש",
   "SearchSubdirectory": "חפש בתת-תיקיות",
   "SecAbbreviation": "שנ'.",
   "SelectAll": "בחר הכל",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Görgetés",
   "ScrollingDisabled": "Görgetés letiltva",
   "ScrollingEnabled": "Görgetés engedélyezve",
+  "Search": "Keresés",
   "SearchSubdirectory": "Keresés alkönyvtárakban",
   "SecAbbreviation": "2.",
   "SelectAll": "Válassza ki az összeset",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Scorrimento",
   "ScrollingDisabled": "Scorrimento disabilitato",
   "ScrollingEnabled": "Scorrimento abilitato",
+  "Search": "Cerca",
   "SearchSubdirectory": "Cerca sottodirectory",
   "SecAbbreviation": "Sec.",
   "SelectAll": "Seleziona tutto",

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

@@ -316,6 +316,7 @@
   "Scrolling": "スクロール",
   "ScrollingDisabled": "スクロール無効",
   "ScrollingEnabled": "スクロール有効",
+  "Search": "検索",
   "SearchSubdirectory": "サブディレクトリを検索",
   "SecAbbreviation": "秒",
   "SelectAll": "すべて選択",

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

@@ -316,6 +316,7 @@
   "Scrolling": "스크롤",
   "ScrollingDisabled": "스크롤 사용 안 함",
   "ScrollingEnabled": "스크롤 사용함",
+  "Search": "검색",
   "SearchSubdirectory": "하위 디렉터리 검색",
   "SecAbbreviation": "초",
   "SelectAll": "전체 선택",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Scrollen",
   "ScrollingDisabled": "Scrollen uitgeschakeld",
   "ScrollingEnabled": "Scrollen ingeschakeld",
+  "Search": "Zoeken",
   "SearchSubdirectory": "Zoek in subdirectories",
   "SecAbbreviation": "Sec.",
   "SelectAll": "Alles selecteren",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Przewijanie",
   "ScrollingDisabled": "Przewijanie wyłączone",
   "ScrollingEnabled": "Przewijanie włączone",
+  "Search": "Szukaj",
   "SearchSubdirectory": "Przeszukaj podkatalogi",
   "SecAbbreviation": "Sek.",
   "SelectAll": "Zaznacz wszystko",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Rolagem",
   "ScrollingDisabled": "Rolagem desativada",
   "ScrollingEnabled": "Rolagem ativada",
+  "Search": "Pesquisar",
   "SearchSubdirectory": "Pesquisar subdiretórios",
   "SecAbbreviation": "Sel.",
   "SelectAll": "Selecionar tudo",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Defilare",
   "ScrollingDisabled": "Defilare dezactivată",
   "ScrollingEnabled": "Defilare activată",
+  "Search": "Căutare",
   "SearchSubdirectory": "Căutare subdirectoare",
   "SecAbbreviation": "Sec.",
   "SelectAll": "Selectează tot",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Прокрутка",
   "ScrollingDisabled": "Прокрутка отключена",
   "ScrollingEnabled": "Прокрутка включена",
+  "Search": "Поиск",
   "SearchSubdirectory": "Искать в подкаталогах",
   "SecAbbreviation": "Sec.",
   "SelectAll": "Выбрать все",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Скроловање",
   "ScrollingDisabled": "Скроловање онемогућено",
   "ScrollingEnabled": "Скроловање омогућено",
+  "Search": "Претрага",
   "SearchSubdirectory": "Претражи поддиректоријуме",
   "SecAbbreviation": "Сек.",
   "SelectAll": "Изабери све",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Rullar",
   "ScrollingDisabled": "Rullning av",
   "ScrollingEnabled": "Rullning på",
+  "Search": "Sök",
   "SearchSubdirectory": "Sök i undermappar",
   "SecAbbreviation": "Sek.",
   "SelectAll": "Välj alla",

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

@@ -316,6 +316,7 @@
   "Scrolling": "Kaydırma",
   "ScrollingDisabled": "Kaydırma devre dışı",
   "ScrollingEnabled": "Kaydırma etkin",
+  "Search": "Ara",
   "SearchSubdirectory": "Alt dizinlerde ara",
   "SecAbbreviation": "Sny.",
   "SelectAll": "Hepsini seç",

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

@@ -316,6 +316,7 @@
   "Scrolling": "滚动",
   "ScrollingDisabled": "已禁用滚动",
   "ScrollingEnabled": "已启用滚动",
+  "Search": "搜索",
   "SearchSubdirectory": "包含子目录中的图片",
   "SecAbbreviation": "秒",
   "SelectAll": "全选",

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

@@ -316,6 +316,7 @@
   "Scrolling": "滾動",
   "ScrollingDisabled": "已禁用滾動",
   "ScrollingEnabled": "已啟用滾動",
+  "Search": "搜尋",
   "SearchSubdirectory": "包含子目錄中的圖片",
   "SecAbbreviation": "秒",
   "SelectAll": "全選",

+ 9 - 0
src/PicView.Core/FileSearch/FileSearchResult.cs

@@ -0,0 +1,9 @@
+namespace PicView.Core.FileSearch;
+
+/// <summary>
+/// Represents a file search result, including the file and the location of the matched text.
+/// </summary>
+/// <param name="File">The matched file.</param>
+/// <param name="MatchStartIndex">The starting index of the match within the file's name. Is -1 if the match is not in the name.</param>
+/// <param name="MatchEndIndex">The exclusive end index of the match within the file's name. Is -1 if the match is not in the name.</param>
+public readonly record struct FileSearchResult(FileInfo File, int MatchStartIndex, int MatchEndIndex);

+ 72 - 0
src/PicView.Core/FileSearch/FileSearcher.cs

@@ -0,0 +1,72 @@
+using ZLinq;
+
+namespace PicView.Core.FileSearch;
+
+public static class FileSearcher
+{
+    private const int ExactMatchScore = 100;
+    private const int StartsWithScore = 50;
+    private const int ContainsInNameScore = 25;
+    private const int ContainsInPathScore = 10;
+    private const int NoMatchScore = 0;
+
+    public static IEnumerable<FileSearchResult> GetFileSearchResults(List<FileInfo> files, string userInput)
+    {
+        if (string.IsNullOrWhiteSpace(userInput))
+        {
+            return [];
+        }
+
+        return files
+            // Create an intermediate object with the file and the match result (score and index)
+            .Select(f => new { File = f, Match = CalculateMatch(f, userInput) })
+            .AsValueEnumerable()
+            // Filter out files with no match
+            .Where(x => x.Match.Score > NoMatchScore)
+            // Order by the highest score
+            .OrderByDescending(x => x.Match.Score)
+            // Project to the final FileSearchResult type
+            .Select(x => new FileSearchResult(
+                x.File,
+                x.Match.Index,
+                x.Match.Index != -1 ? x.Match.Index + userInput.Length : -1
+            ))
+            .ToArray();
+    }
+
+    /// <summary>
+    /// Calculates a relevance score and finds the match index within the filename.
+    /// </summary>
+    /// <returns>A MatchResult containing the Score and the starting Index of the match in the filename.</returns>
+    private static MatchResult CalculateMatch(FileInfo fileInfo, string input)
+    {
+        // Find the first occurrence of the input string in the filename (case-insensitive)
+        var indexInName = fileInfo.Name.IndexOf(input, StringComparison.OrdinalIgnoreCase);
+
+        if (indexInName == -1)
+        {
+            // Match not in name, check the full path as a fallback
+            return fileInfo.Name.Contains(input, StringComparison.OrdinalIgnoreCase)
+                ? new MatchResult(ContainsInPathScore, -1)
+                : new MatchResult(NoMatchScore, -1);
+        }
+
+        // Exact match (highest score)
+        if (fileInfo.Name.Length == input.Length)
+        {
+            return new MatchResult(ExactMatchScore, indexInName);
+        }
+
+        // Starts with or Contains in name
+        return indexInName == 0
+            ? new MatchResult(StartsWithScore, indexInName)
+            : new MatchResult(ContainsInNameScore, indexInName);
+    }
+
+    /// <summary>
+    /// Represents the result of a match calculation.
+    /// </summary>
+    /// <param name="Score">The relevance score of the match.</param>
+    /// <param name="Index">The starting index of the match in the filename, or -1 if not in the filename.</param>
+    private readonly record struct MatchResult(int Score, int Index);
+}

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

@@ -322,6 +322,7 @@ public class LanguageModel
     public string? Scrolling { get; set; }
     public string? ScrollingDisabled { get; set; }
     public string? ScrollingEnabled { get; set; }
+    public string? Search { get; set; }
     public string? ScrollToBottom { get; set; }
     public string? ScrollToTop { get; set; }
     public string? ScrollToZoom { get; set; }

+ 3 - 3
src/PicView.Core/Preloading/Preloader.cs

@@ -491,7 +491,7 @@ public class PreLoader(Func<FileInfo, ValueTask<ImageModel>> imageModelLoader) :
     /// <summary>
     /// Asynchronously clears the entire cache, disposing all cached images and canceling any ongoing preload operations.
     /// </summary>
-    public async Task ClearAsync()
+    public async ValueTask ClearAsync()
     {
         try
         {
@@ -541,7 +541,7 @@ public class PreLoader(Func<FileInfo, ValueTask<ImageModel>> imageModelLoader) :
     /// <param name="currentIndex">The index of the currently viewed image, which serves as the center point for preloading.</param>
     /// <param name="reverse">The direction of preloading. <c>false</c> prioritizes loading subsequent images; <c>true</c> prioritizes previous images.</param>
     /// <param name="list">The complete list of image file paths.</param>
-    public async ValueTask PreLoadAsync(int currentIndex, bool reverse, List<FileInfo> list)
+    public async Task PreLoadAsync(int currentIndex, bool reverse, List<FileInfo> list)
     {
         if (list == null)
         {
@@ -597,7 +597,7 @@ public class PreLoader(Func<FileInfo, ValueTask<ImageModel>> imageModelLoader) :
     /// <param name="reverse">Whether to preload in reverse order.</param>
     /// <param name="list">The list of image file paths.</param>
     /// <param name="token">A <see cref="CancellationToken"/> to observe while waiting for tasks to complete.</param>
-    private async ValueTask PreLoadInternalAsync(int currentIndex, bool reverse, List<FileInfo> list,
+    private async Task PreLoadInternalAsync(int currentIndex, bool reverse, List<FileInfo> list,
         CancellationToken token)
     {
         var count = list.Count;

+ 5 - 1
src/PicView.Core/ViewModels/PicViewerModel.cs

@@ -1,5 +1,7 @@
-using ImageMagick;
+using System.Collections.ObjectModel;
+using ImageMagick;
 using PicView.Core.Exif;
+using PicView.Core.FileSearch;
 using PicView.Core.ImageDecoding;
 using PicView.Core.ImageEffects;
 using R3;
@@ -18,6 +20,8 @@ public class PicViewerModel : IDisposable
     
     public BindableReactiveProperty<FileInfo?> FileInfo { get; } = new();
 
+    public BindableReactiveProperty<ObservableCollection<FileSearchResult>> FilteredFileInfos { get; } = new();
+
     /// <summary>
     /// The image's pixel width
     /// </summary>

+ 273 - 273
src/PicView.Core/ViewModels/TranslationViewModel.cs

@@ -14,311 +14,310 @@ public class TranslationViewModel : IDisposable
     {
         var t = TranslationManager.Translation;
 
-        File.Value = string.Concat(t.File[0].ToString().ToUpper(), t.File.AsSpan(1));
-        SelectFile.Value = t.OpenFileDialog;
-        OpenLastFile.Value = t.OpenLastFile;
-        Paste.Value = t.FilePaste;
-        Copy.Value = t.Copy;
-        Reload.Value = t.Reload;
-        Print.Value = t.Print;
-        DeleteFile.Value = t.DeleteFile;
-        PermanentlyDelete.Value = t.PermanentlyDelete;
-        Save.Value = t.Save;
-        CopyFile.Value = t.CopyFile;
-        NewWindow.Value = t.NewWindow;
-        Close.Value = t.Close;
-        CloseGallery.Value = t.CloseGallery;
-        Open.Value = t.Open;
-        OpenFileDialog.Value = t.OpenFileDialog;
-        ShowInFolder.Value = t.ShowInFolder;
-        OpenWith.Value = t.OpenWith;
-        RenameFile.Value = t.RenameFile;
-        DuplicateFile.Value = t.DuplicateFile;
-        RotateLeft.Value = t.RotateLeft;
-        RotateRight.Value = t.RotateRight;
-        Flip.Value = t.Flip;
-        UnFlip.Value = t.Unflip;
-        ShowBottomGallery.Value = t.ShowBottomGallery;
-        HideBottomGallery.Value = t.HideBottomGallery;
-        AutoFitWindow.Value = t.AutoFitWindow;
-        Stretch.Value = t.Stretch;
-        Crop.Value = t.Crop;
-        ResizeImage.Value = t.ResizeImage;
-        GoToImageAtSpecifiedIndex.Value = t.GoToImageAtSpecifiedIndex;
-        ToggleScroll.Value = t.ToggleScroll;
-        ScrollEnabled.Value = t.ScrollingEnabled;
-        ScrollDisabled.Value = t.ScrollingDisabled;
-        ScrollDirection.Value = t.ScrollDirection;
-        Reverse.Value = t.Reverse;
-        Forward.Value = t.Forward;
-        Slideshow.Value = t.Slideshow;
-        Settings.Value = t.Settings;
-        AboutWindow.Value = t.InfoWindow;
-        ImageInfo.Value = t.ImageInfo;
+        _1Star.Value = t._1Star;
+        _2Star.Value = t._2Star;
+        _3Star.Value = t._3Star;
+        _4Star.Value = t._4Star;
+        _5Star.Value = t._5Star;
         About.Value = t.About;
-        ShowAllSettingsWindow.Value = t.ShowAllSettingsWindow;
-        StayTopMost.Value = t.StayTopMost;
-        SearchSubdirectory.Value = t.SearchSubdirectory;
-        ToggleLooping.Value = t.ToggleLooping;
+        AboutWindow.Value = t.InfoWindow;
+        AdjustNavSpeed.Value = t.AdjustNavSpeed;
+        AdjustTimingForSlideshow.Value = t.AdjustTimingForSlideshow;
+        AdjustTimingForZoom.Value = t.AdjustTimingForZoom;
+        AdvanceBy10Images.Value = t.AdvanceBy10Images;
+        AdvanceBy100Images.Value = t.AdvanceBy100Images;
+        AllowZoomOut.Value = t.AllowZoomOut;
+        Altitude.Value = t.Altitude;
+        Appearance.Value = t.Appearance;
         ApplicationShortcuts.Value = t.ApplicationShortcuts;
-        BatchResize.Value = t.BatchResize;
-        Effects.Value = t.Effects;
-        EffectsTooltip.Value = t.EffectsTooltip;
-        FileProperties.Value = t.FileProperties;
-        OptimizeImage.Value = t.OptimizeImage;
-        ImageInfo.Value = t.ImageInfo;
-        FileName.Value = t.FileName;
-        FileSize.Value = t.FileSize;
-        Folder.Value = t.Folder;
-        FullPath.Value = t.FullPath;
-        Created.Value = t.Created;
-        Modified.Value = t.Modified;
-        LastAccessTime.Value = t.LastAccessTime;
-        ConvertTo.Value = t.ConvertTo;
-        NoConversion.Value = t.NoConversion;
-        Resize.Value = t.Resize;
-        NoResize.Value = t.NoResize;
+        ApplicationStartup.Value = t.ApplicationStartup;
         Apply.Value = t.Apply;
-        Cancel.Value = t.Cancel;
+        Ascending.Value = t.Ascending;
+        Authors.Value = t.Authors;
+        AutoFitWindow.Value = t.AutoFitWindow;
+        BatchResize.Value = t.BatchResize;
         BitDepth.Value = t.BitDepth;
-        ReadAbleAspectRatio.Value = t.AspectRatio;
-        Width.Value = t.Width;
-        Height.Value = t.Height;
-        SizeMp.Value = t.SizeMp;
-        Resolution.Value = t.Resolution;
-        PrintSizeIn.Value = t.PrintSizeIn;
-        PrintSizeCm.Value = t.PrintSizeCm;
+        BlackAndWhite.Value = t.BlackAndWhite;
+        Blur.Value = t.Blur;
+        Bottom.Value = t.Bottom;
+        BottomGalleryItemSize.Value = t.BottomGalleryItemSize;
+        BottomGalleryThumbnailStretch.Value = t.BottomGalleryThumbnailStretch;
+        Brightness.Value = t.Brightness;
+        CameraMaker.Value = t.CameraMaker;
+        CameraModel.Value = t.CameraModel;
+        Cancel.Value = t.Cancel;
+        Center.Value = t.Center;
+        CenterWindow.Value = t.CenterWindow;
         Centimeters.Value = t.Centimeters;
-        Inches.Value = t.Inches;
-        SizeTooltip.Value = t.SizeTooltip;
-        Latitude.Value = t.Latitude;
-        Longitude.Value = t.Longitude;
-        Altitude.Value = t.Altitude;
-        Authors.Value = t.Authors;
-        DateTaken.Value = t.DateTaken;
-        Copyright.Value = t.Copyright;
-        ResolutionUnit.Value = t.ResolutionUnit;
+        ChangeBackground.Value = t.ChangeBackground;
+        ChangeKeybindingText.Value = t.ChangeKeybindingText;
+        ChangeKeybindingTooltip.Value = t.ChangeKeybindingTooltip;
+        ChangingThemeRequiresRestart.Value = t.ChangingThemeRequiresRestart;
+        CheckForUpdates.Value = t.CheckForUpdates;
+        Clear.Value = t.Clear;
+        ClearEffects.Value = t.ClearEffects;
+        Close.Value = t.Close;
+        CloseGallery.Value = t.CloseGallery;
+        CloseWindowPrompt.Value = t.CloseWindowPrompt;
+        Color.Value = t.Color;
+        ColorPickerTool.Value = t.ColorPickerTool;
+        ColorPickerToolTooltip.Value = t.ColorPickerToolTooltip;
         ColorRepresentation.Value = t.ColorRepresentation;
+        Comment.Value = t.Comment;
         CompressedBitsPixel.Value = t.CompressedBitsPixel;
         Compression.Value = t.Compression;
-        ExposureTime.Value = t.ExposureTime;
-        Title.Value = t.Title;
-        Subject.Value = t.Subject;
-        Software.Value = t.Software;
-        CameraMaker.Value = t.CameraMaker;
-        CameraModel.Value = t.CameraModel;
-        FocalLength.Value = t.FocalLength;
-        Fnumber.Value = t.FNumber;
-        Fstop.Value = t.Fstop;
-        MaxAperture.Value = t.MaxAperture;
-        ExposureBias.Value = t.ExposureBias;
-        ExposureProgram.Value = t.ExposureProgram;
-        DigitalZoom.Value = t.DigitalZoom;
-        ISOSpeed.Value = t.ISOSpeed;
-        FocalLength35mm.Value = t.FocalLength35mm;
-        MeteringMode.Value = t.MeteringMode;
+        ConstrainBackgroundToImage.Value = t.ConstrainBackgroundToImage;
         Contrast.Value = t.Contrast;
-        Saturation.Value = t.Saturation;
-        Sharpness.Value = t.Sharpness;
-        WhiteBalance.Value = t.WhiteBalance;
-        FlashEnergy.Value = t.FlashEnergy;
-        FlashMode.Value = t.FlashMode;
-        LightSource.Value = t.LightSource;
-        Brightness.Value = t.Brightness;
-        PhotometricInterpretation.Value = t.PhotometricInterpretation;
-        Orientation.Value = t.Orientation;
-        ExifVersion.Value = t.ExifVersion;
-        LensMaker.Value = t.LensMaker;
-        LensModel.Value = t.LensModel;
-        SortFilesBy.Value = t.SortFilesBy;
-        FileExtension.Value = t.FileExtension;
+        ConvertTo.Value = t.ConvertTo;
+        Copies.Value = t.Copies;
+        Copy.Value = t.Copy;
+        CopyFile.Value = t.CopyFile;
+        CopyImage.Value = t.CopyImage;
+        Copyright.Value = t.Copyright;
+        Created.Value = t.Created;
         CreationTime.Value = t.CreationTime;
-        Random.Value = t.Random;
-        Ascending.Value = t.Ascending;
+        Credits.Value = t.Credits;
+        Crop.Value = t.Crop;
+        CtrlToZoom.Value = t.CtrlToZoom;
+        DarkTheme.Value = t.DarkTheme;
+        DateTaken.Value = t.DateTaken;
+        DeleteFile.Value = t.DeleteFile;
         Descending.Value = t.Descending;
-        RecentFiles.Value = t.RecentFiles;
-        SetAsWallpaper.Value = t.SetAsWallpaper;
-        SetAsLockScreenImage.Value = t.SetAsLockScreenImage;
-        Image.Value = t.Image;
-        CopyImage.Value = t.CopyImage;
+        DigitalZoom.Value = t.DigitalZoom;
+        DisableFadeInButtonsOnHover.Value = t.DisableFadeInButtonsOnHover;
+        DoubleClick.Value = t.DoubleClick;
+        Downloading.Value = t.Downloading;
+        DuplicateFile.Value = t.DuplicateFile;
+        Edit.Value = t.Edit;
+        Effects.Value = t.Effects;
+        EffectsTooltip.Value = t.EffectsTooltip;
+        ExifVersion.Value = t.ExifVersion;
+        ExpandedGalleryItemSize.Value = t.ExpandedGalleryItemSize;
+        ExposureBias.Value = t.ExposureBias;
+        ExposureProgram.Value = t.ExposureProgram;
+        ExposureTime.Value = t.ExposureTime;
+        File.Value = string.Concat(t.File[0].ToString().ToUpper(), t.File.AsSpan(1));
+        FileAssociations.Value = t.FileAssociations;
+        FileConversion.Value = t.FileConversion;
         FileCopyPath.Value = t.FileCopyPath;
         FileCut.Value = t.Cut;
-        CtrlToZoom.Value = t.CtrlToZoom;
-        ScrollToZoom.Value = t.ScrollToZoom;
+        FileExtension.Value = t.FileExtension;
+        FileManagement.Value = t.FileManagement;
+        FileName.Value = t.FileName;
+        FileProperties.Value = t.FileProperties;
+        FileSize.Value = t.FileSize;
+        Fill.Value = t.Fill;
+        FillSquare.Value = t.FillSquare;
+        Filter.Value = t.Filter;
+        FirstImage.Value = t.FirstImage;
+        Fit.Value = t.Fit;
+        FlashEnergy.Value = t.FlashEnergy;
+        FlashMode.Value = t.FlashMode;
+        Flip.Value = t.Flip;
+        Fnumber.Value = t.FNumber;
+        FocalLength.Value = t.FocalLength;
+        FocalLength35mm.Value = t.FocalLength35mm;
+        Folder.Value = t.Folder;
+        Forward.Value = t.Forward;
+        Fstop.Value = t.Fstop;
+        Fullscreen.Value = t.Fullscreen;
+        FullPath.Value = t.FullPath;
+        GallerySettings.Value = t.GallerySettings;
+        GalleryThumbnailStretch.Value = t.GalleryThumbnailStretch;
         GeneralSettings.Value = t.GeneralSettings;
-        Appearance.Value = t.Appearance;
+        GenerateThumbnails.Value = t.GenerateThumbnails;
+        GithubRepo.Value = t.GithubRepo;
+        GlassTheme.Value = t.GlassTheme;
+        GoBackBy10Images.Value = t.GoBackBy10Images;
+        GoBackBy100Images.Value = t.GoBackBy100Images;
+        GoToImageAtSpecifiedIndex.Value = t.GoToImageAtSpecifiedIndex;
+        Height.Value = t.Height;
+        Help.Value = t.Help;
+        HideBottomGallery.Value = t.HideBottomGallery;
+        HideBottomToolbar.Value = t.HideBottomToolbar;
+        HideUI.Value = t.HideUI;
+        HighlightColor.Value = t.HighlightColor;
+        HighQuality.Value = t.HighQuality;
+        Image.Value = t.Image;
+        ImageAliasing.Value = t.ImageAliasing;
+        ImageControl.Value = t.ImageControl;
+        ImageFormat.Value = t.ImageFormat;
+        ImageInfo.Value = t.ImageInfo;
+        Inches.Value = t.Inches;
+        InterfaceConfiguration.Value = t.InterfaceConfiguration;
+        ISOSpeed.Value = t.ISOSpeed;
         Language.Value = t.Language;
-        MouseWheel.Value = t.MouseWheel;
+        LastAccessTime.Value = t.LastAccessTime;
+        LastImage.Value = t.LastImage;
+        Latitude.Value = t.Latitude;
+        Left.Value = t.Left;
+        LensMaker.Value = t.LensMaker;
+        LensModel.Value = t.LensModel;
+        Lighting.Value = t.Lighting;
+        LightSource.Value = t.LightSource;
+        LightTheme.Value = t.LightTheme;
+        Longitude.Value = t.Longitude;
+        Lossless.Value = t.Lossless;
+        Lossy.Value = t.Lossy;
+        Margins.Value = t.Margins;
+        MaxAperture.Value = t.MaxAperture;
+        Maximize.Value = t.Maximize;
+        MeteringMode.Value = t.MeteringMode;
         MiscSettings.Value = t.MiscSettings;
-        StayCentered.Value = t.StayCentered;
-        ShowFileSavingDialog.Value = t.ShowFileSavingDialog;
-        OpenInSameWindow.Value = t.OpenInSameWindow;
-        ApplicationStartup.Value = t.ApplicationStartup;
-        None.Value = t.None;
-        AdjustTimingForSlideshow.Value = t.AdjustTimingForSlideshow;
-        AdjustTimingForZoom.Value = t.AdjustTimingForZoom;
-        AdjustNavSpeed.Value = t.AdjustNavSpeed;
-        SecAbbreviation.Value = t.SecAbbreviation;
-        ResetButtonText.Value = t.ResetButtonText;
-        ShowBottomToolbar.Value = t.ShowBottomToolbar;
-        ShowBottomGalleryWhenUiIsHidden.Value = t.ShowBottomGalleryWhenUiIsHidden;
-        ChangeKeybindingTooltip.Value = t.ChangeKeybindingTooltip;
-        ToggleTaskbarProgress.Value = t.ToggleTaskbarProgress;
-        ChangeKeybindingText.Value = t.ChangeKeybindingText;
+        Modified.Value = t.Modified;
+        Mouse.Value = t.Mouse;
+        MouseDrag.Value = t.MouseDrag;
+        MouseSideButtons.Value = t.MouseSideButtons;
+        MouseWheel.Value = t.MouseWheel;
+        MoveToRecycleBin.Value = t.MoveToRecycleBin;
+        MoveWindow.Value = t.MoveWindow;
+        Navigate.Value = t.Navigate;
+        NavigateBackwards.Value = t.NavigateBackwards;
+        NavigateBetweenDirectories.Value = t.NavigateBetweenDirectories;
+        NavigateFileHistory.Value = t.NavigateFileHistory;
+        NavigateForwards.Value = t.NavigateForwards;
         Navigation.Value = t.Navigation;
-        NextImage.Value = t.NextImage;
-        PrevImage.Value = t.PrevImage;
-        LastImage.Value = t.LastImage;
-        FirstImage.Value = t.FirstImage;
+        NegativeColors.Value = t.NegativeColors;
+        NewWindow.Value = t.NewWindow;
         NextFolder.Value = t.NextFolder;
+        NextImage.Value = t.NextImage;
+        NoConversion.Value = t.NoConversion;
+        None.Value = t.None;
+        NoResize.Value = t.NoResize;
+        Normal.Value = t.Normal;
+        NormalWindow.Value = t.NormalWindow;
+        OldMovie.Value = t.OldMovie;
+        Open.Value = t.Open;
+        OpenFileDialog.Value = t.OpenFileDialog;
+        OpenFileHistory.Value = t.OpenFileHistory;
+        OpenInSameWindow.Value = t.OpenInSameWindow;
+        OpenLastFile.Value = t.OpenLastFile;
+        OpenWith.Value = t.OpenWith;
+        OptimizeImage.Value = t.OptimizeImage;
+        Orientation.Value = t.Orientation;
+        OutputFolder.Value = t.OutputFolder;
+        Pan.Value = t.Pan;
+        PaperSize.Value = t.PaperSize;
+        Paste.Value = t.FilePaste;
+        PencilSketch.Value = t.PencilSketch;
+        Percentage.Value = t.Percentage;
+        PermanentlyDelete.Value = t.PermanentlyDelete;
+        PhotometricInterpretation.Value = t.PhotometricInterpretation;
+        Pin.Value = t.Pin;
+        Pinned.Value = t.Pinned;
+        Pixels.Value = t.Pixels;
+        Posterize.Value = t.Posterize;
         PrevFolder.Value = t.PrevFolder;
-        SelectGalleryThumb.Value = t.SelectGalleryThumb;
+        PrevImage.Value = t.PrevImage;
+        Print.Value = t.Print;
+        Printer.Value = t.Printer;
+        PrintSizeCm.Value = t.PrintSizeCm;
+        PrintSizeIn.Value = t.PrintSizeIn;
+        Quality.Value = t.Quality;
+        Random.Value = t.Random;
+        ReadAbleAspectRatio.Value = t.AspectRatio;
+        RecentFiles.Value = t.RecentFiles;
+        Reload.Value = t.Reload;
+        RemoveImageData.Value = t.RemoveImageData;
+        RemoveStarRating.Value = t.RemoveStarRating;
+        RenameFile.Value = t.RenameFile;
+        Reset.Value = t.Reset;
+        ResetButtonText.Value = t.ResetButtonText;
+        ResetZoom.Value = t.ResetZoom;
+        Resize.Value = t.Resize;
+        ResizeImage.Value = t.ResizeImage;
+        Resolution.Value = t.Resolution;
+        ResolutionUnit.Value = t.ResolutionUnit;
+        RestoreDown.Value = t.RestoreDown;
+        Reverse.Value = t.Reverse;
+        Right.Value = t.Right;
+        RotateLeft.Value = t.RotateLeft;
+        RotateRight.Value = t.RotateRight;
+        Saturation.Value = t.Saturation;
+        Save.Value = t.Save;
+        SaveAs.Value = t.SaveAs;
+        Scale.Value = t.Scale;
         ScrollAndRotate.Value = t.ScrollAndRotate;
-        ScrollUp.Value = t.ScrollUp;
+        ScrollDirection.Value = t.ScrollDirection;
+        ScrollDisabled.Value = t.ScrollingDisabled;
         ScrollDown.Value = t.ScrollDown;
-        ScrollToTop.Value = t.ScrollToTop;
+        ScrollEnabled.Value = t.ScrollingEnabled;
         ScrollToBottom.Value = t.ScrollToBottom;
-        Zoom.Value = t.Zoom;
-        ZoomIn.Value = t.ZoomIn;
-        ZoomOut.Value = t.ZoomOut;
-        Pan.Value = t.Pan;
-        ResetZoom.Value = t.ResetZoom;
-        ImageControl.Value = t.ImageControl;
-        ImageFormat.Value = t.ImageFormat;
-        ChangeBackground.Value = t.ChangeBackground;
-        InterfaceConfiguration.Value = t.InterfaceConfiguration;
-        FileManagement.Value = t.FileManagement;
-        ToggleFullscreen.Value = t.ToggleFullscreen;
-        Fullscreen.Value = t.Fullscreen;
-        ShowImageGallery.Value = t.ShowImageGallery;
-        WindowManagement.Value = t.WindowManagement;
-        CenterWindow.Value = t.CenterWindow;
-        WindowScaling.Value = t.WindowScaling;
-        NormalWindow.Value = t.NormalWindow;
+        ScrollToTop.Value = t.ScrollToTop;
+        ScrollToZoom.Value = t.ScrollToZoom;
+        ScrollUp.Value = t.ScrollUp;
+        Search.Value = t.Search;
+        SearchSubdirectory.Value = t.SearchSubdirectory;
+        SecAbbreviation.Value = t.SecAbbreviation;
+        SelectAll.Value = t.SelectAll;
+        SelectFile.Value = t.OpenFileDialog;
+        SelectFileTypesToAssociate.Value = t.SelectFileTypesToAssociate;
+        SelectGalleryThumb.Value = t.SelectGalleryThumb;
+        SetAsLockScreenImage.Value = t.SetAsLockScreenImage;
+        SetAsWallpaper.Value = t.SetAsWallpaper;
         SetStarRating.Value = t.SetStarRating;
-        _1Star.Value = t._1Star;
-        _2Star.Value = t._2Star;
-        _3Star.Value = t._3Star;
-        _4Star.Value = t._4Star;
-        _5Star.Value = t._5Star;
-        RemoveImageData.Value = t.RemoveImageData;
-        RemoveStarRating.Value = t.RemoveStarRating;
-        Theme.Value = t.Theme;
-        DarkTheme.Value = t.DarkTheme;
-        LightTheme.Value = t.LightTheme;
-        MouseDrag.Value = t.MouseDrag;
-        DoubleClick.Value = t.DoubleClick;
-        MoveWindow.Value = t.MoveWindow;
-        GithubRepo.Value = t.GithubRepo;
-        Version.Value = t.Version;
-        ViewLicenseFile.Value = t.ViewLicenseFile;
-        CheckForUpdates.Value = t.CheckForUpdates;
-        Credits.Value = t.Credits;
-        ColorPickerTool.Value = t.ColorPickerTool;
-        ColorPickerToolTooltip.Value = t.ColorPickerToolTooltip;
-        ExpandedGalleryItemSize.Value = t.ExpandedGalleryItemSize;
-        BottomGalleryItemSize.Value = t.BottomGalleryItemSize;
-        Square.Value = t.Square;
-        Uniform.Value = t.Uniform;
-        UniformToFill.Value = t.UniformToFill;
-        FillSquare.Value = t.FillSquare;
-        Fill.Value = t.Fill;
-        GallerySettings.Value = t.GallerySettings;
-        GalleryThumbnailStretch.Value = t.GalleryThumbnailStretch;
-        BottomGalleryThumbnailStretch.Value = t.BottomGalleryThumbnailStretch;
-        RestoreDown.Value = t.RestoreDown;
+        Settings.Value = t.Settings;
+        Sharpness.Value = t.Sharpness;
+        ShowAllSettingsWindow.Value = t.ShowAllSettingsWindow;
+        ShowBottomGallery.Value = t.ShowBottomGallery;
+        ShowBottomGalleryWhenUiIsHidden.Value = t.ShowBottomGalleryWhenUiIsHidden;
+        ShowBottomToolbar.Value = t.ShowBottomToolbar;
+        ShowConfirmationDialogWhenMovingFileToRecycleBin.Value = t.ShowConfirmationDialogWhenMovingFileToRecycleBin;
+        ShowConfirmationDialogWhenPermanentlyDeletingFile.Value = t.ShowConfirmationDialogWhenPermanentlyDeletingFile;
+        ShowConfirmationOnEsc.Value = t.ShowConfirmationOnEsc;
+        ShowFadeInButtonsOnHover.Value = t.ShowFadeInButtonsOnHover;
+        ShowFileSavingDialog.Value = t.ShowFileSavingDialog;
+        ShowImageGallery.Value = t.ShowImageGallery;
+        ShowInFolder.Value = t.ShowInFolder;
+        ShowUI.Value = t.ShowUI;
+        ShowZoomPercentagePopup.Value = t.ShowZoomPercentagePopup;
         SideBySide.Value = t.SideBySide;
         SideBySideTooltip.Value = t.SideBySideTooltip;
-        HighlightColor.Value = t.HighlightColor;
-        AllowZoomOut.Value = t.AllowZoomOut;
-        GlassTheme.Value = t.GlassTheme;
-        ChangingThemeRequiresRestart.Value = t.ChangingThemeRequiresRestart;
-        ShowUI.Value = t.ShowUI;
-        HideUI.Value = t.HideUI;
-        HideBottomToolbar.Value = t.HideBottomToolbar;
-        Center.Value = t.Center;
-        Tile.Value = t.Tile;
-        Fit.Value = t.Fit;
-        Pixels.Value = t.Pixels;
-        Percentage.Value = t.Percentage;
-        Quality.Value = t.Quality;
-        SaveAs.Value = t.SaveAs;
-        Reset.Value = t.Reset;
-        AdvanceBy10Images.Value = t.AdvanceBy10Images;
-        AdvanceBy100Images.Value = t.AdvanceBy100Images;
-        GoBackBy10Images.Value = t.GoBackBy10Images;
-        GoBackBy100Images.Value = t.GoBackBy100Images;
-        ShowFadeInButtonsOnHover.Value = t.ShowFadeInButtonsOnHover;
-        DisableFadeInButtonsOnHover.Value = t.DisableFadeInButtonsOnHover;
-        UsingTouchpad.Value = t.UsingTouchpad;
-        UsingMouse.Value = t.UsingMouse;
+        SizeMp.Value = t.SizeMp;
+        SizeTooltip.Value = t.SizeTooltip;
+        Slideshow.Value = t.Slideshow;
+        Software.Value = t.Software;
+        Solarize.Value = t.Solarize;
+        SortFilesBy.Value = t.SortFilesBy;
         SourceFolder.Value = t.SourceFolder;
-        OutputFolder.Value = t.OutputFolder;
-        GenerateThumbnails.Value = t.GenerateThumbnails;
-        Lossless.Value = t.Lossless;
-        Lossy.Value = t.Lossy;
+        Square.Value = t.Square;
         Start.Value = t.Start;
+        StayCentered.Value = t.StayCentered;
+        StayTopMost.Value = t.StayTopMost;
+        Stretch.Value = t.Stretch;
+        Subject.Value = t.Subject;
+        Theme.Value = t.Theme;
         Thumbnail.Value = t.Thumbnail;
-        WidthAndHeight.Value = t.WidthAndHeight;
-        CloseWindowPrompt.Value = t.CloseWindowPrompt;
-        ShowConfirmationOnEsc.Value = t.ShowConfirmationOnEsc;
-        ImageAliasing.Value = t.ImageAliasing;
-        HighQuality.Value = t.HighQuality;
-        Lighting.Value = t.Lighting;
-        BlackAndWhite.Value = t.BlackAndWhite;
-        NegativeColors.Value = t.NegativeColors;
-        Blur.Value = t.Blur;
-        PencilSketch.Value = t.PencilSketch;
-        OldMovie.Value = t.OldMovie;
-        Posterize.Value = t.Posterize;
-        ClearEffects.Value = t.ClearEffects;
-        Solarize.Value = t.Solarize;
-        Maximize.Value = t.Maximize;
-        SelectAll.Value = t.SelectAll;
-        Normal.Value = t.Normal;
-        FileAssociations.Value = t.FileAssociations;
-        FileConversion.Value = t.FileConversion;
-        SelectFileTypesToAssociate.Value = t.SelectFileTypesToAssociate;
-        Filter.Value = t.Filter;
-        UnselectAll.Value = t.UnselectAll;
+        Tile.Value = t.Tile;
+        Title.Value = t.Title;
+        ToggleFullscreen.Value = t.ToggleFullscreen;
+        ToggleLooping.Value = t.ToggleLooping;
+        ToggleScroll.Value = t.ToggleScroll;
+        ToggleTaskbarProgress.Value = t.ToggleTaskbarProgress;
+        Top.Value = t.Top;
         Unassociate.Value = t.Unassociate;
-        ShowConfirmationDialogWhenMovingFileToRecycleBin.Value = t.ShowConfirmationDialogWhenMovingFileToRecycleBin;
-        MoveToRecycleBin.Value = t.MoveToRecycleBin;
-        ShowConfirmationDialogWhenPermanentlyDeletingFile.Value = t.ShowConfirmationDialogWhenPermanentlyDeletingFile;
-        Downloading.Value = t.Downloading;
-        Pinned.Value = t.Pinned;
+        UnFlip.Value = t.Unflip;
+        Uniform.Value = t.Uniform;
+        UniformToFill.Value = t.UniformToFill;
         Unpin.Value = t.Unpin;
-        Pin.Value = t.Pin;
-        Clear.Value = t.Clear;
-        OpenFileHistory.Value = t.OpenFileHistory;
-        ConstrainBackgroundToImage.Value = t.ConstrainBackgroundToImage;
-        Window.Value = t.Window;
-        WindowMargin.Value = t.WindowMargin;
-        Mouse.Value = t.Mouse;
-        MouseSideButtons.Value = t.MouseSideButtons;
-        NavigateFileHistory.Value = t.NavigateFileHistory;
-        NavigateBetweenDirectories.Value = t.NavigateBetweenDirectories;
-        NavigateForwards.Value = t.NavigateForwards;
-        NavigateBackwards.Value = t.NavigateBackwards;
-        Comment.Value = t.Comment;
-        Navigate.Value = t.Navigate;
-        Help.Value = t.Help;
-        View.Value = t.View;
-        Edit.Value = t.Edit;
-        ShowZoomPercentagePopup.Value = t.ShowZoomPercentagePopup;
+        UnselectAll.Value = t.UnselectAll;
         UseAnimatedZoom.Value = t.UseAnimatedZoom;
+        UsingMouse.Value = t.UsingMouse;
+        UsingTouchpad.Value = t.UsingTouchpad;
+        Version.Value = t.Version;
+        View.Value = t.View;
+        ViewLicenseFile.Value = t.ViewLicenseFile;
         WhenDeletingAFile.Value = t.WhenDeletingAFile;
-
-        Printer.Value = t.Printer;
-        PaperSize.Value = t.PaperSize;
-        Scale.Value = t.Scale;
-        Color.Value = t.Color;
-        Copies.Value = t.Copies;
-        Margins.Value = t.Margins;
-        Top.Value = t.Top;
-        Bottom.Value = t.Bottom;
-        Left.Value = t.Left;
-        Right.Value = t.Right;
+        WhiteBalance.Value = t.WhiteBalance;
+        Width.Value = t.Width;
+        WidthAndHeight.Value = t.WidthAndHeight;
+        Window.Value = t.Window;
+        WindowManagement.Value = t.WindowManagement;
+        WindowMargin.Value = t.WindowMargin;
+        WindowScaling.Value = t.WindowScaling;
+        Zoom.Value = t.Zoom;
+        ZoomIn.Value = t.ZoomIn;
+        ZoomOut.Value = t.ZoomOut;
     }
 
     #region Static Translation Strings
@@ -546,6 +545,7 @@ public class TranslationViewModel : IDisposable
     public BindableReactiveProperty<string?> ScrollToTop { get; } = new();
     public BindableReactiveProperty<string?> ScrollToZoom { get; } = new();
     public BindableReactiveProperty<string?> ScrollUp { get; } = new();
+    public BindableReactiveProperty<string?> Search { get; } = new();
     public BindableReactiveProperty<string?> SearchSubdirectory { get; } = new();
     public BindableReactiveProperty<string?> SecAbbreviation { get; } = new();
     public BindableReactiveProperty<string?> SelectAll { get; } = new();