Selaa lähdekoodia

wip PalettesBrowser

Krzysztof Krysiński 2 vuotta sitten
vanhempi
commit
d3a173c056

+ 40 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/PaletteHelpers.cs

@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using Avalonia.Platform.Storage;
+using PixiEditor.Extensions.Palettes.Parsers;
+using PixiEditor.Models.IO;
+
+namespace PixiEditor.Helpers;
+
+internal static class PaletteHelpers
+{
+    public static List<FilePickerFileType> GetFilter(IList<PaletteFileParser> parsers, bool includeCommon)
+    {
+        List<FilePickerFileType> filePickerFileTypes = new();
+
+        if (includeCommon)
+        {
+            List<string> allSupportedFormats = new();
+            foreach (var parser in parsers)
+            {
+                allSupportedFormats.AddRange(parser.SupportedFileExtensions);
+            }
+
+            string allSupportedFormatsString = string.Join(';', allSupportedFormats).Replace(".", "*.");
+            filePickerFileTypes.Add(new FilePickerFileType($"Palette Files ({allSupportedFormatsString}")
+            {
+                Patterns = allSupportedFormats
+            });
+        }
+
+        foreach (var parser in parsers)
+        {
+            string supportedFormats = string.Join(';', parser.SupportedFileExtensions).Replace(".", "*.");
+            filePickerFileTypes.Add(new FilePickerFileType($"{parser.FileName} ({supportedFormats})")
+            {
+                Patterns = parser.SupportedFileExtensions
+            });
+        }
+
+        return filePickerFileTypes;
+    }
+}

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Palettes/PaletteList.cs

@@ -6,5 +6,5 @@ namespace PixiEditor.Models.DataHolders.Palettes;
 internal sealed class PaletteList : ObservableObject
 {
     public bool FetchedCorrectly { get; set; } = false;
-    public ObservableCollection<Palette> Palettes { get; set; } = new ObservableCollection<Palette>();
+    public ObservableRangeCollection<Palette> Palettes { get; set; } = new ObservableRangeCollection<Palette>();
 }

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SystemCommands.cs

@@ -19,7 +19,7 @@ public static class SystemCommands
 
     public static ICommand CloseWindowCommand { get; } = new RelayCommand<Window>(CloseWindow);
 
-    private static void CloseWindow(Window? obj)
+    public static void CloseWindow(Window? obj)
     {
         // TODO: Close window, this is just a placeholder, won't work
     }

+ 148 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/PalettesBrowser.axaml

@@ -0,0 +1,148 @@
+<Window
+    x:Class="PixiEditor.Views.Dialogs.PalettesBrowser"
+    x:ClassModifier="internal"
+    xmlns="https://github.com/avaloniaui"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+    xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+    xmlns:dialogs1="clr-namespace:PixiEditor.Views.Dialogs"
+    xmlns:viewModels="clr-namespace:PixiEditor.Avalonia.ViewModels"
+    mc:Ignorable="d"
+    WindowStartupLocation="CenterScreen"
+    Height="600" Width="850"
+    x:Name="palettesBrowser"
+    FlowDirection="{helpers:Localization FlowDirection}"
+    ui:Translator.Key="PALETTE_BROWSER">
+    <!--<WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>-->
+
+    <Grid Background="{DynamicResource ThemeBackgroundBrush1}" Focusable="True" PointerPressed="Grid_MouseDown">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="45"/>
+            <RowDefinition Height="1*"/>
+        </Grid.RowDefinitions>
+
+        <dialogs1:DialogTitleBar TitleKey="PALETTE_BROWSER" CloseCommand="{x:Static viewModels:SystemCommands.CloseWindowCommand}"/>
+
+        <DockPanel Background="{StaticResource MainColor}" Grid.Row="1">
+            <StackPanel HorizontalAlignment="Left" Margin="10" Orientation="Horizontal" VerticalAlignment="Center">
+                <Label ui:Translator.Key="SORT_BY" VerticalAlignment="Center"/>
+                <ComboBox x:Name="sortingComboBox" VerticalAlignment="Center" SelectionChanged="SortingComboBox_SelectionChanged">
+                    <ComboBoxItem IsSelected="True" ui:Translator.Key="DEFAULT"/>
+                    <ComboBoxItem ui:Translator.Key="ALPHABETICAL"/>
+                    <ComboBoxItem ui:Translator.Key="COLOR_COUNT"/>
+                </ComboBox>
+                <ToggleButton Margin="10 0 0 0" x:Name="toggleBtn"
+                              IsChecked="{Binding SortAscending, ElementName=palettesBrowser}"
+                              Focusable="False">
+                    <Image Width="24" Height="24" Source="/Images/ChevronsDown.png">
+                        <Image.Styles>
+                            <Style Selector="{x:Type Image}">
+                                <Setter Property="RenderTransform">
+                                    <Setter.Value>
+
+                                    </Setter.Value>
+                                </Setter>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding IsChecked, ElementName=toggleBtn}" Value="true">
+                                        <Setter Property="RenderTransform">
+                                            <Setter.Value>
+                                                <RotateTransform Angle="180" CenterX="11.5" CenterY="11.5"/>
+                                            </Setter.Value>
+                                        </Setter>
+                                        <Setter Property="ui:Translator.TooltipKey" Value="ASCENDING"/>
+                                    </DataTrigger>
+                                    <DataTrigger Binding="{Binding IsChecked, ElementName=toggleBtn}" Value="false">
+                                        <Setter Property="ui:Translator.TooltipKey" Value="DESCENDING"/>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Image.Styles>
+                    </Image>
+                </ToggleButton>
+                <Label Margin="10 0 0 0" ui:Translator.Key="NAME" VerticalAlignment="Center"/>
+                <usercontrols:InputBox
+                                       Text="{Binding NameFilter, Delay=100, ElementName=palettesBrowser, UpdateSourceTrigger=PropertyChanged}"
+                                       VerticalAlignment="Center"
+                                       Style="{StaticResource DarkTextBoxStyle}" Width="150">
+                    <Interaction.Behaviors>
+                        <behaviours:TextBoxFocusBehavior SelectOnMouseClick="True" ConfirmOnEnter="True"
+                                                         FocusNext="{Binding ElementName=numberInput, Path=FocusNext}"/>
+                        <behaviours:GlobalShortcutFocusBehavior/>
+                    </Interaction.Behaviors>
+                </usercontrols:InputBox>
+
+                <Label Margin="10 0 0 0" ui:Translator.Key="COLORS" Style="{StaticResource BaseLabel}" VerticalAlignment="Center"/>
+                <ComboBox x:Name="colorsComboBox" VerticalAlignment="Center" SelectionChanged="ColorsComboBox_SelectionChanged">
+                    <ComboBoxItem IsSelected="True" ui:Translator.Key="ANY"/>
+                    <ComboBoxItem ui:Translator.Key="MAX"/>
+                    <ComboBoxItem ui:Translator.Key="MIN"/>
+                    <ComboBoxItem ui:Translator.Key="EXACT"/>
+                </ComboBox>
+                <usercontrols:NumberInput Width="50" VerticalAlignment="Center" Margin="10 0 0 0"
+                                   FocusNext="True"
+                                   Value="{Binding ColorsNumber, ElementName=palettesBrowser, Mode=TwoWay}"/>
+                <CheckBox Margin="10 0 0 0" VerticalAlignment="Center"
+                          IsChecked="{Binding ShowOnlyFavourites, ElementName=palettesBrowser}" ui:Translator.Key="FAVORITES"/>
+            </StackPanel>
+            <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 10 0">
+                <Button ui:Translator.TooltipKey="ADD_FROM_CURRENT_PALETTE" Command="{Binding ElementName=palettesBrowser, Path=AddFromPaletteCommand}"
+                        Cursor="Hand" Margin="10 0" Width="24" Height="24">
+                    <Image Source="/Images/Plus-square.png"/>
+                </Button>
+                <Button Cursor="Hand" Click="OpenFolder_OnClick" Width="24" Height="24"
+                        ui:Translator.TooltipKey="OPEN_PALETTES_DIR_TOOLTIP">
+                    <Image Source="/Images/Folder.png"/>
+                </Button>
+                <Button HorizontalAlignment="Right" Margin="10 0 0 0" ui:Translator.TooltipKey="BROWSE_ON_LOSPEC_TOOLTIP"
+                        Width="24" Height="24"
+                        Click="BrowseOnLospec_OnClick"
+                        CommandParameter="https://lospec.com/palette-list">
+                    <Image Source="/Images/Globe.png"/>
+                </Button>
+                <Button HorizontalAlignment="Right" Margin="10 0 0 0" ui:Translator.TooltipKey="IMPORT_FROM_FILE_TOOLTIP"
+                        Width="24" Height="24"
+                        Click="ImportFromFile_OnClick">
+                    <Image Source="/Images/hard-drive.png"/>
+                </Button>
+            </StackPanel>
+        </DockPanel>
+        <Grid Grid.Row="2" Margin="10">
+            <TextBlock ui:Translator.Key="COULD_NOT_LOAD_PALETTE" Foreground="White" FontSize="20" HorizontalAlignment="Center"
+                       VerticalAlignment="Center" IsVisible="{Binding !Visibility, ElementName=itemsControl}"/>
+            <StackPanel Panel.ZIndex="10" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center"
+                        IsVisible="{Binding ElementName=palettesBrowser, Path=SortedResults.Count, Converter={converters:CountToVisibilityConverter}}">
+                <TextBlock ui:Translator.Key="NO_PALETTES_FOUND" Foreground="White" FontSize="20" TextAlignment="Center"/>
+                <TextBlock Margin="0 10 0 0">
+                    <Hyperlink Foreground="Gray" Cursor="Hand" FontSize="18" NavigateUri="https://lospec.com/palette-list"
+                               RequestNavigate="Hyperlink_OnRequestNavigate">
+                        <TextBlock ui:Translator.Key="LOSPEC_LINK_TEXT"/>
+                    </Hyperlink>
+                </TextBlock>
+                <Image Width="128" Height="128" Source="/Images/Search.png"/>
+            </StackPanel>
+            <ScrollViewer x:Name="scrollViewer" Margin="5" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" ScrollChanged="ScrollViewer_ScrollChanged">
+                <ItemsControl x:Name="itemsControl" ItemsSource="{Binding SortedResults, ElementName=palettesBrowser}"
+                              IsVisible="{Binding PaletteList.FetchedCorrectly, ElementName=palettesBrowser}">
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate>
+                            <local:PaletteItem Palette="{Binding}"
+                                               OnRename="PaletteItem_OnRename"
+                                               ToggleFavouriteCommand="{Binding ToggleFavouriteCommand, ElementName=palettesBrowser}"
+                                               DeletePaletteCommand="{Binding DeletePaletteCommand, ElementName=palettesBrowser}"
+                                               ImportPaletteCommand="{Binding ImportPaletteCommand, ElementName=palettesBrowser}"/>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                </ItemsControl>
+            </ScrollViewer>
+            <Image gif:ImageBehavior.AnimatedSource="/Images/Processing.gif" HorizontalAlignment="Center" VerticalAlignment="Center"
+                   IsVisible="{Binding IsFetching, ElementName=palettesBrowser}"
+                   Height="50" gif:ImageBehavior.AnimationSpeedRatio="1.5"/>
+        </Grid>
+    </Grid>
+</Window>

+ 697 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/PalettesBrowser.axaml.cs

@@ -0,0 +1,697 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Avalonia.Helpers.Extensions;
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.Extensions;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Extensions.Palettes;
+using PixiEditor.Extensions.Palettes.Parsers;
+using PixiEditor.Helpers;
+using PixiEditor.Models.AppExtensions.Services;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Palettes;
+using PixiEditor.Models.DataProviders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.IO;
+using PixiEditor.OperatingSystem;
+using PaletteColor = PixiEditor.Extensions.Palettes.PaletteColor;
+
+namespace PixiEditor.Views.Dialogs;
+
+internal partial class PalettesBrowser : Window, IPopupWindow
+{
+    public static string UniqueId => "PixiEditor.BrowserPalette";
+    string IPopupWindow.UniqueId => UniqueId;
+
+    private const int ItemsPerLoad = 25;
+
+    private readonly LocalizedString[] stopItTexts = new[]
+    {
+        new LocalizedString("STOP_IT_TEXT1"),
+        new LocalizedString("STOP_IT_TEXT2"),
+        new LocalizedString("STOP_IT_TEXT3"),
+        new LocalizedString("STOP_IT_TEXT4"),
+    };
+
+    public PaletteList PaletteList
+    {
+        get => (PaletteList)GetValue(PaletteListProperty);
+        set => SetValue(PaletteListProperty, value);
+    }
+
+    public static readonly StyledProperty<PaletteList> PaletteListProperty =
+        AvaloniaProperty.Register<PalettesBrowser, PaletteList>(nameof(PaletteList));
+
+    public ICommand ImportPaletteCommand
+    {
+        get => (ICommand)GetValue(ImportPaletteCommandProperty);
+        set => SetValue(ImportPaletteCommandProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand> ImportPaletteCommandProperty =
+        AvaloniaProperty.Register<PalettesBrowser, ICommand>(nameof(ImportPaletteCommand));
+
+    public static readonly StyledProperty<ICommand> DeletePaletteCommandProperty =
+        AvaloniaProperty.Register<PalettesBrowser, ICommand>(nameof(DeletePaletteCommand));
+
+    public ICommand DeletePaletteCommand
+    {
+        get => (ICommand)GetValue(DeletePaletteCommandProperty);
+        set => SetValue(DeletePaletteCommandProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand> AddFromPaletteCommandProperty =
+        AvaloniaProperty.Register<PalettesBrowser, ICommand>(nameof(AddFromPaletteCommand));
+
+    public ICommand AddFromPaletteCommand
+    {
+        get { return (ICommand)GetValue(AddFromPaletteCommandProperty); }
+        set { SetValue(AddFromPaletteCommandProperty, value); }
+    }
+
+    public bool IsFetching
+    {
+        get => GetValue(IsFetchingProperty);
+        set => SetValue(IsFetchingProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> IsFetchingProperty =
+        AvaloniaProperty.Register<PalettesBrowser, bool>(nameof(IsFetching));
+
+    public int ColorsNumber
+    {
+        get => (int)GetValue(ColorsNumberProperty);
+        set => SetValue(ColorsNumberProperty, value);
+    }
+
+    public static readonly StyledProperty<int> ColorsNumberProperty =
+        AvaloniaProperty.Register<PalettesBrowser, int>(nameof(ColorsNumber), 8);
+    public bool SortAscending
+    {
+        get => (bool)GetValue(SortAscendingProperty);
+        set => SetValue(SortAscendingProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> SortAscendingProperty =
+        AvaloniaProperty.Register<PalettesBrowser, bool>(nameof(SortAscending), true);
+
+
+    public static readonly StyledProperty<ObservableRangeCollection<Palette>> SortedResultsProperty =
+        AvaloniaProperty.Register<PalettesBrowser, ObservableRangeCollection<Palette>>(nameof(SortedResults), new ObservableRangeCollection<Palette>());
+
+    public ObservableRangeCollection<Palette> SortedResults
+    {
+        get => GetValue(SortedResultsProperty);
+        set => SetValue(SortedResultsProperty, value);
+    }
+
+    public static readonly StyledProperty<string> NameFilterProperty =
+        AvaloniaProperty.Register<PalettesBrowser, string>(nameof(NameFilter), string.Empty);
+
+    public string NameFilter
+    {
+        get => (string)GetValue(NameFilterProperty);
+        set => SetValue(NameFilterProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> ShowOnlyFavouritesProperty =
+        AvaloniaProperty.Register<PalettesBrowser, bool>(nameof(ShowOnlyFavourites), false);
+
+    public bool ShowOnlyFavourites
+    {
+        get => (bool)GetValue(ShowOnlyFavouritesProperty);
+        set => SetValue(ShowOnlyFavouritesProperty, value);
+    }
+
+    public static readonly StyledProperty<PaletteProvider> PaletteProviderProperty =
+        AvaloniaProperty.Register<PalettesBrowser, PaletteProvider>(nameof(PaletteProvider));
+
+    public PaletteProvider PaletteProvider
+    {
+        get { return GetValue(PaletteProviderProperty); }
+        set { SetValue(PaletteProviderProperty, value); }
+    }
+    public RelayCommand<Palette> ToggleFavouriteCommand { get; set; }
+    public int SortingIndex { get; set; } = 0;
+    public ColorsNumberMode ColorsNumberMode { get; set; } = ColorsNumberMode.Any;
+
+    private FilteringSettings filteringSettings;
+
+    public FilteringSettings Filtering => filteringSettings ??=
+        new FilteringSettings(ColorsNumberMode, ColorsNumber, NameFilter, ShowOnlyFavourites,
+            IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes, new List<string>()));
+
+    private char[] separators = new char[] { ' ', ',' };
+
+    private SortingType InternalSortingType => (SortingType)SortingIndex;
+    public ObservableRangeCollection<PaletteColor> CurrentEditingPalette { get; set; }
+    public static PalettesBrowser Instance { get; internal set; }
+
+    private LocalPalettesFetcher LocalPalettesFetcher
+    {
+        get
+        {
+            return localPalettesFetcher ??= (LocalPalettesFetcher)PaletteProvider.DataSources.First(x => x is LocalPalettesFetcher);
+        }
+    }
+
+    private LocalPalettesFetcher localPalettesFetcher;
+
+    private ILocalizationProvider localizationProvider;
+
+    private double _lastScrolledOffset = -1;
+
+    static PalettesBrowser()
+    {
+        ColorsNumberProperty.Changed.Subscribe(ColorsNumberChanged);
+        SortAscendingProperty.Changed.Subscribe(OnSortAscendingChanged);
+        NameFilterProperty.Changed.Subscribe(OnNameFilterChanged);
+        ShowOnlyFavouritesProperty.Changed.Subscribe(OnShowOnlyFavouritesChanged);
+    }
+
+    public PalettesBrowser(PaletteProvider provider)
+    {
+        localizationProvider = ViewModelMain.Current.LocalizationProvider;
+        localizationProvider.OnLanguageChanged += LocalizationProviderOnOnLanguageChanged;
+        MinWidth = DetermineWidth();
+        
+        PaletteProvider = provider;
+        InitializeComponent();
+        Title = new LocalizedString("PALETTE_BROWSER");
+        Instance = this;
+
+        DeletePaletteCommand = new AsyncRelayCommand<Palette>(DeletePalette, CanDeletePalette);
+        ToggleFavouriteCommand = new RelayCommand<Palette>(ToggleFavourite, CanToggleFavourite);
+        AddFromPaletteCommand = new AsyncRelayCommand(AddFromCurrentPalette, CanAddFromPalette);
+        Loaded += async (_, _) =>
+        {
+            await UpdatePaletteList();
+            LocalPalettesFetcher.CacheUpdated += LocalCacheRefreshed;
+        };
+        Closed += (_, _) =>
+        {
+            Instance = null;
+            LocalPalettesFetcher.CacheUpdated -= LocalCacheRefreshed;
+        };
+
+        IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, OnFavouritePalettesChanged);
+    }
+
+    public async Task<bool?> ShowDialog()
+    {
+        bool result = false;
+        await Application.Current.ForDesktopMainWindowAsync(async (window) =>
+        {
+            result = await ShowDialog<bool>(window);
+        });
+
+        return result;
+    }
+
+    private void LocalizationProviderOnOnLanguageChanged(Language obj)
+    {
+        MinWidth = DetermineWidth();
+    }
+
+    private double DetermineWidth()
+    {
+        return localizationProvider.CurrentLanguage.LanguageData.Code switch
+        {
+            "ru" or "uk" => 900,
+            _ => 850
+        };
+    } 
+
+    private bool CanAddFromPalette()
+    {
+        return CurrentEditingPalette != null;
+    }
+
+    private bool CanDeletePalette(Palette palette)
+    {
+        return palette != null && palette.Source.GetType() == typeof(LocalPalettesFetcher);
+    }
+
+    private void OnFavouritePalettesChanged(object obj)
+    {
+        Filtering.Favourites =
+            IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
+    }
+
+    public static PalettesBrowser Open(PaletteProvider provider, ICommand importPaletteCommand, ObservableRangeCollection<PaletteColor> currentEditingPalette)
+    {
+        if (Instance != null) return Instance;
+
+        Window owner = null;
+        if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+        {
+            owner = desktop.MainWindow;
+        }
+
+        PalettesBrowser browser = new PalettesBrowser(provider)
+        {
+            Owner = owner,
+            ImportPaletteCommand = importPaletteCommand,
+            CurrentEditingPalette = currentEditingPalette
+        };
+
+        browser.Show();
+        return browser;
+    }
+
+    private async void LocalCacheRefreshed(RefreshType refreshType, Palette itemAffected, string fileNameAffected)
+    {
+        await Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            SortedResults ??= new ObservableRangeCollection<Palette>();
+            switch (refreshType)
+            {
+                case RefreshType.All:
+                    await UpdatePaletteList();
+                    break;
+                case RefreshType.Created:
+                    HandleCachePaletteCreated(itemAffected);
+                    break;
+                case RefreshType.Updated:
+                    HandleCacheItemUpdated(itemAffected);
+                    break;
+                case RefreshType.Deleted:
+                    HandleCacheItemDeleted(fileNameAffected);
+                    break;
+                case RefreshType.Renamed:
+                    HandleCacheItemRenamed(itemAffected, fileNameAffected);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(refreshType), refreshType, null);
+            }
+
+        });
+    }
+
+    private void HandleCacheItemRenamed(Palette itemAffected, string oldFileName)
+    {
+        var old = SortedResults.FirstOrDefault(x => x.FileName == oldFileName);
+        if (old != null)
+        {
+            old.Name = itemAffected.Name;
+            old.FileName = itemAffected.FileName;
+        }
+
+        UpdateRenamedFavourite(Path.GetFileNameWithoutExtension(oldFileName), itemAffected.Name);
+        Sort();
+    }
+
+    private void HandleCacheItemDeleted(string deletedItemFileName)
+    {
+        Palette item = SortedResults.FirstOrDefault(x => x.FileName == deletedItemFileName);
+        if (item != null)
+        {
+            SortedResults.Remove(item);
+            PaletteList.Palettes.Remove(item);
+        }
+    }
+
+    private void HandleCacheItemUpdated(Palette updatedItem)
+    {
+        var item = SortedResults.FirstOrDefault(x => x.FileName == updatedItem.FileName);
+        if (item is null)
+            return;
+
+        item.Name = updatedItem.Name;
+        item.IsFavourite = updatedItem.IsFavourite;
+        item.Colors = updatedItem.Colors;
+
+        Sort();
+    }
+
+    private void HandleCachePaletteCreated(Palette updatedItem)
+    {
+        SortedResults.Add(updatedItem);
+        PaletteList.Palettes.Add(updatedItem);
+        Sort();
+    }
+
+    private async void ToggleFavourite(Palette palette)
+    {
+        palette.IsFavourite = !palette.IsFavourite;
+        var favouritePalettes = IPreferences.Current.GetLocalPreference(PreferencesConstants.FavouritePalettes, new List<string>());
+
+        if (palette.IsFavourite && !favouritePalettes.Contains(palette.Name))
+        {
+            favouritePalettes.Add(palette.Name);
+        }
+        else
+        {
+            favouritePalettes.RemoveAll(x => x == palette.Name);
+        }
+
+        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favouritePalettes);
+        await UpdatePaletteList();
+    }
+
+    private bool IsPaletteFavourite(string name)
+    {
+        var favouritePalettes = IPreferences.Current.GetLocalPreference(PreferencesConstants.FavouritePalettes, new List<string>());
+        return favouritePalettes.Contains(name);
+    }
+
+    private async Task DeletePalette(Palette palette)
+    {
+        if (palette == null) return;
+
+        string filePath = Path.Join(Paths.PathToPalettesFolder, palette.FileName);
+        if (File.Exists(filePath))
+        {
+            if (await ConfirmationDialog.Show("DELETE_PALETTE_CONFIRMATION", "WARNING") == ConfirmationType.Yes)
+            {
+                _ = LocalPalettesFetcher.DeletePalette(palette.FileName);
+                RemoveFavouritePalette(palette);
+            }
+        }
+    }
+
+    private static void RemoveFavouritePalette(Palette palette)
+    {
+        var favouritePalettes =
+            IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
+        if (favouritePalettes != null && favouritePalettes.Contains(palette.Name))
+        {
+            favouritePalettes.Remove(palette.Name);
+            IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favouritePalettes);
+        }
+    }
+
+    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private static async void OnShowOnlyFavouritesChanged(AvaloniaPropertyChangedEventArgs<bool> e)
+    {
+        PalettesBrowser browser = (PalettesBrowser)e.Sender;
+        browser.Filtering.ShowOnlyFavourites = e.NewValue.Value;
+        await browser.UpdatePaletteList();
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }
+
+    private static async void ColorsNumberChanged(AvaloniaPropertyChangedEventArgs<int> e)
+    {
+        PalettesBrowser browser = (PalettesBrowser)e.Sender;
+        browser.Filtering.ColorsCount = e.NewValue.Value;
+        await browser.UpdatePaletteList();
+    }
+
+    private static void OnSortAscendingChanged(AvaloniaPropertyChangedEventArgs<bool> d)
+    {
+        PalettesBrowser browser = d.Sender as PalettesBrowser;
+        browser.Sort();
+    }
+
+    public async Task UpdatePaletteList()
+    {
+        IsFetching = true;
+        _lastScrolledOffset = -1;
+        PaletteList?.Palettes?.Clear();
+        PaletteList src = await FetchPaletteList(Filtering);
+        if (PaletteList == null)
+        {
+            PaletteList = src;
+        }
+        else
+        {
+            AddToPaletteList(src.Palettes);
+        }
+
+        Sort();
+
+        IsFetching = false;
+    }
+
+    private void AddToPaletteList(ObservableRangeCollection<Palette> srcPalettes)
+    {
+        if (srcPalettes == null)
+            return;
+
+        foreach (var pal in srcPalettes)
+        {
+            if(PaletteEquals(pal, PaletteList.Palettes)) continue;
+            PaletteList.Palettes.Add(pal);
+        }
+    }
+
+    private async Task<PaletteList> FetchPaletteList(FilteringSettings filtering)
+    {
+        int startIndex = PaletteList != null ? PaletteList.Palettes.Count : 0;
+        var src = await PaletteProvider.FetchPalettes(startIndex, ItemsPerLoad, filtering);
+        ObservableRangeCollection<Palette> palettes = new ObservableRangeCollection<Palette>();
+        if (src != null)
+        {
+            foreach (var pal in src)
+            {
+                palettes.Add(new Palette(pal.Name, pal.Colors, pal.FileName, pal.Source) { IsFavourite = IsPaletteFavourite(pal.Name) });
+            }
+        }
+
+        PaletteList list = new PaletteList { Palettes = palettes, FetchedCorrectly = src != null };
+        return list;
+    }
+
+    private static async void OnNameFilterChanged(AvaloniaPropertyChangedEventArgs<string> e)
+    {
+        var browser = (PalettesBrowser)e.Sender;
+        browser.Filtering.Name = browser.NameFilter;
+        await browser.UpdatePaletteList();
+        browser.scrollViewer.ScrollToHome();
+    }
+
+    private async void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
+    {
+        if (PaletteList?.Palettes == null) return;
+        var viewer = (ScrollViewer)sender;
+        if (viewer.VerticalOffset == viewer.ScrollableHeight && _lastScrolledOffset != viewer.VerticalOffset)
+        {
+            IsFetching = true;
+            var newPalettes = await FetchPaletteList(Filtering);
+            if (newPalettes is not { FetchedCorrectly: true } || newPalettes.Palettes == null)
+            {
+                IsFetching = false;
+                return;
+            }
+
+            AddToPaletteList(newPalettes.Palettes);
+            Sort();
+            IsFetching = false;
+
+            _lastScrolledOffset = viewer.VerticalOffset;
+        }
+    }
+
+    private bool PaletteEquals(Palette palette, ObservableRangeCollection<Palette> paletteListPalettes)
+    {
+        return paletteListPalettes.Any(x => x.Name == palette.Name && x.Source == palette.Source && x.Colors.Count == palette.Colors.Count
+        && x.Colors.SequenceEqual(palette.Colors));
+    }
+
+    private void SortingComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (e.AddedItems is { Count: > 0 } && e.AddedItems[0] is ComboBoxItem)
+        {
+            var comboBox = (ComboBox)sender;
+            SortingIndex = comboBox.SelectedIndex;
+            Sort();
+            scrollViewer?.ScrollToHome();
+        }
+    }
+
+    private async void ColorsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (Instance != null && e.AddedItems is { Count: > 0 } && e.AddedItems[0] is ComboBoxItem)
+        {
+            var comboBox = (ComboBox)sender;
+            ColorsNumberMode = (ColorsNumberMode)comboBox.SelectedIndex;
+            Filtering.ColorsNumberMode = ColorsNumberMode;
+            await UpdatePaletteList();
+
+            scrollViewer?.ScrollToHome();
+        }
+    }
+
+    private bool CanToggleFavourite(Palette palette)
+    {
+        return palette is { Colors.Count: > 0 };
+    }
+
+    private void Grid_MouseDown(object? sender, PointerPressedEventArgs pointerPressedEventArgs)
+    {
+        ((Grid)sender).Focus();
+    }
+
+    private void Sort()
+    {
+        Sort(!SortAscending);
+    }
+
+    private void Sort(bool descending)
+    {
+        if (PaletteList?.Palettes == null) return;
+
+        SortedResults?.Clear();
+
+        IOrderedEnumerable<Palette> sorted = null;
+        if (!descending)
+        {
+            switch (InternalSortingType)
+            {
+                case Models.DataHolders.Palettes.SortingType.Default:
+                    sorted = PaletteList.Palettes.OrderByDescending(x => x.IsFavourite).ThenBy(x => PaletteList.Palettes.IndexOf(x));
+                    break;
+                case Models.DataHolders.Palettes.SortingType.Alphabetical:
+                    sorted = PaletteList.Palettes.OrderBy(x => x.Name);
+                    break;
+                case Models.DataHolders.Palettes.SortingType.ColorCount:
+                    sorted = PaletteList.Palettes.OrderBy(x => x.Colors.Count);
+                    break;
+            }
+        }
+        else
+        {
+            switch (InternalSortingType)
+            {
+                case Models.DataHolders.Palettes.SortingType.Default:
+                    sorted = PaletteList.Palettes.OrderByDescending(x => PaletteList.Palettes.IndexOf(x));
+                    break;
+                case Models.DataHolders.Palettes.SortingType.Alphabetical:
+                    sorted = PaletteList.Palettes.OrderByDescending(x => x.Name);
+                    break;
+                case Models.DataHolders.Palettes.SortingType.ColorCount:
+                    sorted = PaletteList.Palettes.OrderByDescending(x => x.Colors.Count);
+                    break;
+            }
+        }
+
+        if (sorted != null)
+        {
+            SortedResults = new ObservableRangeCollection<Palette>(sorted);
+        }
+    }
+
+    private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (Directory.Exists(Paths.PathToPalettesFolder))
+        {
+            IOperatingSystem.Current.OpenFolder(Paths.PathToPalettesFolder);
+        }
+    }
+
+    private async Task AddFromCurrentPalette()
+    {
+        if (CurrentEditingPalette?.Count == 0)
+            return;
+
+        string finalFileName = LocalPalettesFetcher.GetNonExistingName($"{new LocalizedString("UNNAMED_PALETTE").Value}.pal", true);
+        await LocalPalettesFetcher.SavePalette(finalFileName, CurrentEditingPalette.ToArray());
+    }
+
+    private void PaletteItem_OnRename(object sender, EditableTextBlock.TextChangedEventArgs e)
+    {
+        PaletteItem item = (PaletteItem)sender;
+        item.Palette.Name = e.OldText;
+
+        if (string.IsNullOrWhiteSpace(e.NewText) || e.NewText == item.Palette.Name || e.NewText.Length > 50)
+            return;
+
+        string oldFileName = $"{e.OldText}.pal";
+
+        string finalNewName = LocalPalettesFetcher.GetNonExistingName($"{Palette.ReplaceInvalidChars(e.NewText)}.pal", true);
+        string newPath = Path.Join(Paths.PathToPalettesFolder, finalNewName);
+
+        if (newPath.Length > 250)
+        {
+            NoticeDialog.Show(stopItTexts[Random.Shared.Next(stopItTexts.Length - 1)], "NAME_IS_TOO_LONG");
+            return;
+        }
+
+        LocalPalettesFetcher.RenamePalette(oldFileName, finalNewName);
+    }
+
+    private static void UpdateRenamedFavourite(string old, string newName)
+    {
+        var favourites = IPreferences.Current.GetLocalPreference(
+            PreferencesConstants.FavouritePalettes,
+            new List<string>());
+
+        if (favourites.Contains(old))
+        {
+            favourites.Remove(old);
+            favourites.Add(newName);
+        }
+
+        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.FavouritePalettes, favourites);
+    }
+
+    private void BrowseOnLospec_OnClick(object sender, RoutedEventArgs e)
+    {
+        Button button = sender as Button;
+        string url = (string)button.CommandParameter;
+
+        IOperatingSystem.Current.OpenHyperlink(url);
+    }
+
+
+    private async void ImportFromFile_OnClick(object? sender, RoutedEventArgs e)
+    {
+        var parsers = PaletteProvider.AvailableParsers;
+        var file = await TopLevel.GetTopLevel(this).StorageProvider.OpenFilePickerAsync(
+            new FilePickerOpenOptions()
+            {
+                FileTypeFilter = PaletteHelpers.GetFilter(parsers, true), AllowMultiple = false
+            });
+
+        if (file is { Count: > 0 })
+        {
+            var fileName = file[0].Path.AbsolutePath;
+            await ImportPalette(fileName, parsers);
+        }
+    }
+
+    private async Task ImportPalette(string fileName, IList<PaletteFileParser> parsers)
+    {
+        var parser = parsers.FirstOrDefault(x => x.SupportedFileExtensions.Contains(Path.GetExtension(fileName)));
+        if (parser != null)
+        {
+            var data = await parser.Parse(fileName);
+
+            if (data.IsCorrupted) return;
+            string name = LocalPalettesFetcher.GetNonExistingName(Path.GetFileName(fileName), true);
+            await LocalPalettesFetcher.SavePalette(name, data.Colors.ToArray());
+        }
+    }
+
+    private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e)
+    {
+        IOperatingSystem.Current.OpenHyperlink(e.Uri.ToString());
+    }
+
+    protected override void OnClosing(WindowClosingEventArgs e)
+    {
+        base.OnClosing(e);
+        IPreferences.Current.RemoveCallback(PreferencesConstants.FavouritePalettes, OnFavouritePalettesChanged);
+    }
+}

+ 1 - 1
src/PixiEditor.Extensions/Windowing/IPopupWindow.cs

@@ -6,7 +6,7 @@ public interface IPopupWindow
     public string Title { get; set; }
     public void Show();
     public void Close();
-    public bool? ShowDialog();
+    public Task<bool?> ShowDialog();
     public double Width { get; set; }
     public double Height { get; set; }
 }