diff --git a/LapisItemEditor/LapisItemEditor.csproj b/LapisItemEditor/LapisItemEditor.csproj index 9d4c8ad..f6e2f53 100644 --- a/LapisItemEditor/LapisItemEditor.csproj +++ b/LapisItemEditor/LapisItemEditor.csproj @@ -1,4 +1,4 @@ - + Exe net6.0 @@ -11,6 +11,7 @@ + diff --git a/LapisItemEditor/ViewModels/Main/ItemListViewModel.cs b/LapisItemEditor/ViewModels/Main/ItemListViewModel.cs index 6ece1e3..f79ff0b 100644 --- a/LapisItemEditor/ViewModels/Main/ItemListViewModel.cs +++ b/LapisItemEditor/ViewModels/Main/ItemListViewModel.cs @@ -6,29 +6,112 @@ using DynamicData; using DynamicData.Binding; using System.Reactive.Linq; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Windows.Input; namespace LapisItemEditor.ViewModels + { + + public sealed class ItemListViewModel : ViewModelBase { private SourceList _items; private ReadOnlyObservableCollection _observableItems; + private bool searchForServerId; + private string searchQuery; public ReactiveCommand ItemTypeSelected { get; } + public ICommand Search { get; } + + public string SearchQuery { get => searchQuery; set => this.RaiseAndSetIfChanged(ref searchQuery, value); } + public bool SearchForServerId { get => searchForServerId; set => this.RaiseAndSetIfChanged(ref searchForServerId, value); } + + + public class ItemModelComparer : IEqualityComparer + { + Func f; + public ItemModelComparer(Func f) + { + this.f = f; + } + + public bool Equals(ItemModel? x, ItemModel? y) + { + return f(x).Equals(f(y)); + } + + public int GetHashCode([DisallowNull] ItemModel obj) + { + return (int)obj.ClientId; + } + } + + public int getIndex(uint id, bool clientId = true) + { + var item = new ItemModel(); + Func f; + if (clientId) + { + f = (a) => a.ClientId; + item.ClientId = id; + } + else + { + f = (a) => a.ServerId; + item.ServerId = id; + } + + return Items.AsObservableList().Items.IndexOf(item, new ItemModelComparer(f)); + } + + public ItemListViewModel() { ItemTypeSelected = ReactiveCommand.Create(itemModel => { return itemModel; }); Items = new SourceList(); + var searchFilter = this.WhenValueChanged(x => x.SearchQuery) + .Select(SearchNamePredicate); + + var sorter = this.WhenValueChanged(x => x.SearchQuery) + .Select(SortComparer); + var loader = Items .Connect() - .Sort(SortExpressionComparer.Ascending(x => x.ServerId)) + .Filter(searchFilter) + .Sort(sorter) .ObserveOn(RxApp.MainThreadScheduler) .Bind(out _observableItems) .DisposeMany().Subscribe(); } + private Func SearchNamePredicate(string query) + { + bool isInt = int.TryParse(query, out var result); + if (isInt || query == null || query.Length < 3) + { + return model => true; + } + + return model => FuzzySharp.Fuzz.Ratio(query, model.Name) >= 0.8; + } + + private IComparer SortComparer(string query) + { + bool isInt = int.TryParse(query, out var result); + if (isInt || query == null || query.Length < 3) + { + return SortExpressionComparer.Ascending(x => x.ServerId); + } + else + { + return SortExpressionComparer.Descending(model => FuzzySharp.Fuzz.Ratio(query, model.Name)); + } + } + public SourceList Items { get => _items; diff --git a/LapisItemEditor/Views/Main/ItemListView.axaml b/LapisItemEditor/Views/Main/ItemListView.axaml index ffdd366..ab452bb 100644 --- a/LapisItemEditor/Views/Main/ItemListView.axaml +++ b/LapisItemEditor/Views/Main/ItemListView.axaml @@ -59,20 +59,29 @@ - - - - - + + + + + Server ID + + + + + + + + + \ No newline at end of file diff --git a/LapisItemEditor/Views/Main/ItemListView.axaml.cs b/LapisItemEditor/Views/Main/ItemListView.axaml.cs index d94796b..43b7811 100644 --- a/LapisItemEditor/Views/Main/ItemListView.axaml.cs +++ b/LapisItemEditor/Views/Main/ItemListView.axaml.cs @@ -1,21 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Threading; using LapisItemEditor.ViewModels; using static LapisItemEditor.ViewModels.ItemListViewModel; namespace LapisItemEditor.Views { - public partial class ItemListView : ReactiveUserControl + + public partial class ItemListView : ReactiveUserControl { + + public static Action Debounce(Action f, int milliseconds = 300) + { + var last = 0; + return () => + { + var current = Interlocked.Increment(ref last); + + Task.Delay(milliseconds).ContinueWith(task => + { + if (current == last) + { + Dispatcher.UIThread.Post(() => f()); + } + task.Dispose(); + }); + }; + } + private ScrollViewer scrollView; + private Action debouncedSearch; + private Action debouncedWrapper; + + public TextBox SearchBox => this.FindControl("search_box"); + + public ItemListView() { InitializeComponent(); + + scrollView = this.FindControl("scroller"); + debouncedWrapper = Debounce(search, 300); + } + + private void search() + { + if (ViewModel == null) + { + return; + } + + ViewModel.SearchQuery = SearchBox.Text; + + if (int.TryParse(SearchBox.Text, out var result)) + { + bool useClientId = !ViewModel.SearchForServerId; + var index = ViewModel.getIndex((uint)result, useClientId); + + if (index != -1) + { + scrollView.Offset = new Avalonia.Point(0, index * 51); + } + } + else + { + scrollView.Offset = new Avalonia.Point(0, 0); + } } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + + var a = SearchBox; + } public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e) @@ -28,5 +90,13 @@ public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e) e.TemplateKey = "defaultKey"; } + + + + private void searchBoxKeyUp(object sender, KeyEventArgs e) + { + debouncedWrapper(); + } + } }