ui(manager): improve search bindings

This commit is contained in:
2025-11-18 16:21:34 +01:00
parent 86f93cf3d7
commit 2cbe25e0f8
12 changed files with 89 additions and 52 deletions

View File

@@ -70,7 +70,7 @@ internal static class SharedFunctions
foreach (var update in updates) foreach (var update in updates)
{ {
var sourceTag = update.AvailableVersions[update.NewVersion].Tag; var sourceTag = update.AvailableVersions[update.NewVersion].Tag;
if (api.Model.CurrentGridRows?.FirstOrDefault(n => n.Action == update.Origin) is { } row) if (api.Model.CurrentGridRows.List.Items.FirstOrDefault(n => n.Action == update.Origin) is { } row)
row.SourceTag = sourceTag; row.SourceTag = sourceTag;
else else
update.Origin.SourceTag = sourceTag; update.Origin.SourceTag = sourceTag;

View File

@@ -17,10 +17,10 @@ internal class CheckAllActionsHealthyFeature : PluginFunction, IPluginFeaturePro
protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is not MainApiParameters p || p.Api.Model.CurrentGridRows is null) if (@params is not MainApiParameters p)
return null; return null;
await SharedFunctions.CheckActionHealthy(p.Api, [.. p.Api.Model.CurrentGridRows]); await SharedFunctions.CheckActionHealthy(p.Api, [.. p.Api.Model.CurrentGridRows.View]);
return null; return null;
} }

View File

@@ -17,8 +17,8 @@ internal class ClearDirectLinksFeature : PluginFunction, IPluginFeatureProvider<
protected override Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is MainApiParameters p && p.Api.Model.CurrentGridRows is not null) if (@params is MainApiParameters p)
SharedFunctions.ClearDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows]); SharedFunctions.ClearDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows.View]);
return Task.FromResult<object?>(null); return Task.FromResult<object?>(null);
} }
} }

View File

@@ -17,10 +17,10 @@ internal class UpdateDirectLinksFeature : PluginFunction, IPluginFeatureProvider
protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is not MainApiParameters p || p.Api.Model.CurrentGridRows is null) if (@params is not MainApiParameters p)
return null; return null;
await SharedFunctions.FindNewDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows]); await SharedFunctions.FindNewDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows.View]);
return null; return null;
} }

View File

@@ -50,6 +50,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.8" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.8" />
<PackageReference Include="DynamicData" Version="9.4.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.3.0" /> <PackageReference Include="MessageBox.Avalonia" Version="3.3.0" />
<PackageReference Include="EPPlus" Version="8.2.1" /> <PackageReference Include="EPPlus" Version="8.2.1" />
<PackageReference Include="NGitLab" Version="11.0.1" /> <PackageReference Include="NGitLab" Version="11.0.1" />

View File

@@ -120,6 +120,18 @@
<StackPanel <StackPanel
Orientation="Horizontal"> Orientation="Horizontal">
<!-- TextBox: Version -->
<TextBox
Margin="3, 0, 3, 0"
Width="100"
Text="{Binding Version}"/>
<!-- CheckBox: Is public -->
<CheckBox
Margin="3, 0, 3, 0"
Content="{x:Static langRes:GeneralLangRes.Public}"
IsChecked="{Binding IsPublic}"/>
<!-- Button: Add action --> <!-- Button: Add action -->
<pilz:ImageButton <pilz:ImageButton
x:Name="ButtonAddAction" x:Name="ButtonAddAction"
@@ -135,18 +147,6 @@
ImageSource="{x:Static local:MainWindow.ButtonImageRemoveAction}" ImageSource="{x:Static local:MainWindow.ButtonImageRemoveAction}"
Background="Transparent" Background="Transparent"
Click="ButtonRemoveAction_OnClick"/> Click="ButtonRemoveAction_OnClick"/>
<!-- TextBox: Version -->
<TextBox
Margin="3, 0, 3, 0"
Width="100"
Text="{Binding Version}"/>
<!-- CheckBox: Is public -->
<CheckBox
Margin="3, 0, 3, 0"
Content="{x:Static langRes:GeneralLangRes.Public}"
IsChecked="{Binding IsPublic}"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
@@ -172,7 +172,7 @@
Grid.Row="1" Grid.Row="1"
x:Name="DataGridActions" x:Name="DataGridActions"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding CurrentGridRows}" ItemsSource="{Binding CurrentGridRows.View}"
SelectedItem="{Binding SelectedGridRow}"> SelectedItem="{Binding SelectedGridRow}">
<DataGrid.ContextMenu> <DataGrid.ContextMenu>

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using DynamicData;
using ModpackUpdater.Apps.Manager.Api; using ModpackUpdater.Apps.Manager.Api;
using ModpackUpdater.Apps.Manager.Api.Model; using ModpackUpdater.Apps.Manager.Api.Model;
using ModpackUpdater.Apps.Manager.Api.Plugins.Features; using ModpackUpdater.Apps.Manager.Api.Plugins.Features;
@@ -129,7 +130,7 @@ public partial class MainWindow : Window, IMainApi
private async void Window_OnLoaded(object? sender, RoutedEventArgs e) private async void Window_OnLoaded(object? sender, RoutedEventArgs e)
{ {
var updater = new AppUpdates(Program.UpdateUrl, this); var updater = new AppUpdates(Program.UpdateUrl, this);
updater.OnDownloadProgramUpdate += (o, args) => Model.Progress.Start(); updater.OnDownloadProgramUpdate += (o, _) => Model.Progress.Start();
await updater.UpdateApp(); await updater.UpdateApp();
Model.Progress.Stop(); Model.Progress.Stop();
@@ -239,7 +240,7 @@ public partial class MainWindow : Window, IMainApi
return; return;
} }
rows.Add(new MainWindowGridRow(action, rootInfos)); rows.List.Add(new MainWindowGridRow(action, rootInfos));
} }
private void ButtonRemoveAction_OnClick(object? sender, RoutedEventArgs e) private void ButtonRemoveAction_OnClick(object? sender, RoutedEventArgs e)
@@ -262,6 +263,6 @@ public partial class MainWindow : Window, IMainApi
return; return;
} }
rows.Remove(row); rows.List.Remove(row);
} }
} }

View File

@@ -0,0 +1,38 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using DynamicData;
namespace ModpackUpdater.Apps.Manager.Ui.Models;
public class DynamicDataView<T> : INotifyPropertyChanged where T : notnull
{
public event PropertyChangedEventHandler? PropertyChanged;
private string? searchText;
private readonly Subject<string?> searchTextSubject = new();
public SourceList<T> List { get; } = new();
public ReadOnlyObservableCollection<T> View { get; }
public DynamicDataView(Func<string?, Func<T, bool>> predicate)
{
List.Connect()
.Filter(searchTextSubject/*.Throttle(TimeSpan.FromMilliseconds(250))*/.Select(predicate))
.Bind(out var view)
.Subscribe();
searchTextSubject?.OnNext(searchText);
View = view;
}
public string? SearchText
{
get => searchText;
set
{
searchText = value;
searchTextSubject.OnNext(value);
}
}
}

View File

@@ -11,13 +11,13 @@ public class MainWindowViewModel : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<MainWindowTreeNode>? currentTreeNodes; private ObservableCollection<MainWindowTreeNode>? currentTreeNodes;
private ObservableCollection<MainWindowGridRow>? currentGridRows;
private MainWindowTreeNode? selectedTreeNode; private MainWindowTreeNode? selectedTreeNode;
private IWorkspace? currentWorkspace; private IWorkspace? currentWorkspace;
public bool IsUpdate => selectedTreeNode is ActionSetTreeNode node && node.Infos is UpdateInfo; public bool IsUpdate => selectedTreeNode is ActionSetTreeNode node && node.Infos is UpdateInfo;
public ProgressInfos Progress { get; } = new(); public ProgressInfos Progress { get; } = new();
public MainWindowGridRow? SelectedGridRow { get; set; } public MainWindowGridRow? SelectedGridRow { get; set; }
public DynamicDataView<MainWindowGridRow> CurrentGridRows { get; } = new(FilterGridRows);
[AlsoNotifyFor(nameof(CurrentTreeNodes))] [AlsoNotifyFor(nameof(CurrentTreeNodes))]
public IWorkspace? CurrentWorkspace public IWorkspace? CurrentWorkspace
@@ -48,24 +48,23 @@ public class MainWindowViewModel : INotifyPropertyChanged
} }
} }
[AlsoNotifyFor(nameof(CurrentGridRows))]
public MainWindowTreeNode? SelectedTreeNode public MainWindowTreeNode? SelectedTreeNode
{ {
get => selectedTreeNode; get => selectedTreeNode;
set set
{ {
currentGridRows = null;
selectedTreeNode = value; selectedTreeNode = value;
CurrentGridRows.List.Edit(list =>
{
list.Clear();
if (CurrentWorkspace?.InstallInfos != null && selectedTreeNode is ActionSetTreeNode node)
list.AddRange(node.Infos.Actions.Select(n => new MainWindowGridRow(n, CurrentWorkspace.InstallInfos)));
});
} }
} }
public ObservableCollection<MainWindowGridRow>? CurrentGridRows private static Func<MainWindowGridRow, bool> FilterGridRows(string? searchText)
{ {
get return n => string.IsNullOrWhiteSpace(searchText) || true;
{
if (currentGridRows == null && CurrentWorkspace?.InstallInfos != null && selectedTreeNode is ActionSetTreeNode node)
currentGridRows = [.. node.Infos.Actions.Select(n => new MainWindowGridRow(n, CurrentWorkspace.InstallInfos))];
return currentGridRows;
}
} }
} }

View File

@@ -1,5 +1,9 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using DynamicData;
using PropertyChanged; using PropertyChanged;
namespace ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode; namespace ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode;
@@ -9,6 +13,10 @@ public class UpdatesCollectorViewModel : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public ProgressInfos Progress { get; } = new(); public ProgressInfos Progress { get; } = new();
public string? SearchText { get; set; } public DynamicDataView<ModUpdateInfo> Updates { get; } = new(FilterUpdates);
public ObservableCollection<ModUpdateInfo> Updates { get; } = [];
private static Func<ModUpdateInfo, bool> FilterUpdates(string? searchText)
{
return n => string.IsNullOrWhiteSpace(searchText) || (n.Origin.Name != null && n.Origin.Name.Contains(searchText, StringComparison.InvariantCultureIgnoreCase));
}
} }

View File

@@ -26,15 +26,14 @@
<TextBox <TextBox
Grid.Row="0" Grid.Row="0"
Watermark="Search" Watermark="Search"
Text="{Binding SearchText}" Text="{Binding Updates.SearchText}"/>
TextChanged="TextBoxSearch_OnTextChanged"/>
<!-- ScrollViewer: Updates --> <!-- ScrollViewer: Updates -->
<ScrollViewer <ScrollViewer
Grid.Row="1"> Grid.Row="1">
<ItemsControl <ItemsControl
ItemsSource="{Binding Updates}"> ItemsSource="{Binding Updates.View}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
@@ -43,8 +42,7 @@
ColumnDefinitions="20*,20*,20*,Auto" ColumnDefinitions="20*,20*,20*,Auto"
ColumnSpacing="6" ColumnSpacing="6"
RowSpacing="6" RowSpacing="6"
Margin="3" Margin="3">
IsVisible="{Binding Visible}">
<!-- Label: Name --> <!-- Label: Name -->
<TextBlock <TextBlock

View File

@@ -1,5 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using DynamicData;
using ModpackUpdater.Apps.Manager.Api.Model; using ModpackUpdater.Apps.Manager.Api.Model;
using ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode; using ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode;
using ModpackUpdater.Manager; using ModpackUpdater.Manager;
@@ -36,7 +37,7 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
if (updates == null || updates.Length == 0 || updates[0].Tag == action.SourceTag) if (updates == null || updates.Length == 0 || updates[0].Tag == action.SourceTag)
continue; continue;
Model.Updates.Add(new(updates, action)); Model.Updates.List.Add(new(updates, action));
if (IsClosed) if (IsClosed)
break; break;
@@ -47,7 +48,7 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
protected override object GetResult() protected override object GetResult()
{ {
return new ModUpdates(Model.Updates); return new ModUpdates([.. Model.Updates.List.Items]);
} }
private async void Me_OnLoaded(object? sender, RoutedEventArgs e) private async void Me_OnLoaded(object? sender, RoutedEventArgs e)
@@ -55,18 +56,9 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
await FindUpdates(); await FindUpdates();
} }
private void TextBoxSearch_OnTextChanged(object? sender, TextChangedEventArgs e)
{
var searchString = Model.SearchText?.Trim().ToLowerInvariant();
var hasNoSearch = string.IsNullOrWhiteSpace(searchString);
foreach (var item in Model.Updates)
item.Visible = hasNoSearch || (item.Origin.Name != null && item.Origin.Name.Contains(searchString!, StringComparison.InvariantCultureIgnoreCase));
}
private void ButtonRemoveUpdate_Click(object? sender, RoutedEventArgs e) private void ButtonRemoveUpdate_Click(object? sender, RoutedEventArgs e)
{ {
if (sender is Button button && button.DataContext is ModUpdateInfo update) if (sender is Button button && button.DataContext is ModUpdateInfo update)
Model.Updates.Remove(update); Model.Updates.List.Remove(update);
} }
} }