diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/AdvancedCollectionView.cs b/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/AdvancedCollectionView.cs new file mode 100644 index 00000000..d85c64e5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/AdvancedCollectionView.cs @@ -0,0 +1,768 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.WinUI.Collections; +using CommunityToolkit.WinUI.Helpers; +using Microsoft.UI.Xaml.Data; +using Snap.Hutao.Core.ExceptionService; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Reflection; +using System.Runtime.CompilerServices; +using Windows.Foundation; +using Windows.Foundation.Collections; +using NotifyCollectionChangedAction = System.Collections.Specialized.NotifyCollectionChangedAction; + +namespace Snap.Hutao.Control.Collection.AdvancedCollectionView; + +internal sealed class AdvancedCollectionView : IAdvancedCollectionView, INotifyPropertyChanged, ISupportIncrementalLoading, IComparer + where T : class +{ + private readonly List view; + private readonly ObservableCollection sortDescriptions; + private readonly Dictionary sortProperties; + private readonly bool liveShapingEnabled; + private readonly HashSet observedFilterProperties = []; + + private IList source; + private Predicate? filter; + private int deferCounter; + private WeakEventListener, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener; + + public AdvancedCollectionView() + : this(new List(0)) + { + } + + public AdvancedCollectionView(IList source, bool isLiveShaping = false) + { + liveShapingEnabled = isLiveShaping; + view = []; + sortDescriptions = []; + sortDescriptions.CollectionChanged += SortDescriptionsCollectionChanged; + sortProperties = []; + Source = source; + } + + public event EventHandler? CurrentChanged; + + public event CurrentChangingEventHandler? CurrentChanging; + + public event PropertyChangedEventHandler? PropertyChanged; + + public event VectorChangedEventHandler? VectorChanged; + + public IList Source + { + get => source; + + [MemberNotNull(nameof(source))] + set + { + if (ReferenceEquals(source, value)) + { + return; + } + + if (source is not null) + { + DetachPropertyChangedHandler(source); + } + + source = value; + AttachPropertyChangedHandler(source); + + sourceWeakEventListener?.Detach(); + + if (source is INotifyCollectionChanged sourceNotifyCollectionChanged) + { + sourceWeakEventListener = new WeakEventListener, object?, NotifyCollectionChangedEventArgs>(this) + { + // Call the actual collection changed event + OnEventAction = (source, changed, arg3) => SourceNotifyCollectionChangedCollectionChanged(source, arg3), + + // The source doesn't exist anymore + OnDetachAction = (listener) => + { + ArgumentNullException.ThrowIfNull(sourceWeakEventListener); + sourceNotifyCollectionChanged.CollectionChanged -= sourceWeakEventListener.OnEvent; + }, + }; + sourceNotifyCollectionChanged.CollectionChanged += sourceWeakEventListener.OnEvent; + } + + HandleSourceChanged(); + OnPropertyChanged(); + } + } + + public int Count + { + get => view.Count; + } + + public bool IsReadOnly + { + get => source is null || source.IsReadOnly; + } + + public IObservableVector CollectionGroups + { + get => default!; + } + + public T? CurrentItem + { + get => CurrentPosition > -1 && CurrentPosition < view.Count ? view[CurrentPosition] : default; + set => MoveCurrentTo(value); + } + + public int CurrentPosition { get; private set; } + + public bool HasMoreItems + { + get => source is ISupportIncrementalLoading { HasMoreItems: true }; + } + + public bool IsCurrentAfterLast + { + get => CurrentPosition >= view.Count; + } + + public bool IsCurrentBeforeFirst + { + get => CurrentPosition < 0; + } + + public bool CanFilter + { + get => true; + } + + public Predicate? Filter + { + get => filter; + set + { + if (filter == value) + { + return; + } + + filter = value; + HandleFilterChanged(); + } + } + + public bool CanSort + { + get => true; + } + + public IList SortDescriptions + { + get => sortDescriptions; + } + + public IEnumerable SourceCollection + { + get => source; + } + + public IReadOnlyList View + { + get => view; + } + + public T this[int index] + { + get => view[index]; + set => view[index] = value; + } + + public void Refresh() + { + HandleSourceChanged(); + } + + public void RefreshFilter() + { + HandleFilterChanged(); + } + + public void RefreshSorting() + { + HandleSortChanged(); + } + + public IEnumerator GetEnumerator() + { + return view.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return view.GetEnumerator(); + } + + public void Add(T item) + { + ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only."); + source.Add(item); + } + + public void Clear() + { + ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only."); + source.Clear(); + } + + public bool Contains(T item) + { + return view.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + view.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only."); + source.Remove(item); + return true; + } + + [SuppressMessage("", "SH007")] + public int IndexOf(T? item) + { + return view.IndexOf(item!); + } + + public void Insert(int index, T item) + { + ThrowHelper.NotSupportedIf(IsReadOnly, "Collection is read-only."); + source.Insert(index, item); + } + + public void RemoveAt(int index) + { + Remove(view[index]); + } + + public bool MoveCurrentTo(T? item) + { + return (item is not null && item.Equals(CurrentItem)) || MoveCurrentToIndex(IndexOf(item)); + } + + public bool MoveCurrentToPosition(int index) + { + return MoveCurrentToIndex(index); + } + + public bool MoveCurrentToFirst() + { + return MoveCurrentToIndex(0); + } + + public bool MoveCurrentToLast() + { + return MoveCurrentToIndex(view.Count - 1); + } + + public bool MoveCurrentToNext() + { + return MoveCurrentToIndex(CurrentPosition + 1); + } + + public bool MoveCurrentToPrevious() + { + return MoveCurrentToIndex(CurrentPosition - 1); + } + + public IAsyncOperation? LoadMoreItemsAsync(uint count) + { + return (source as ISupportIncrementalLoading)?.LoadMoreItemsAsync(count); + } + + public void ObserveFilterProperty(string propertyName) + { + observedFilterProperties.Add(propertyName); + } + + public void ClearObservedFilterProperties() + { + observedFilterProperties.Clear(); + } + + public IDisposable DeferRefresh() + { + return new NotificationDeferrer(this); + } + + int IComparer.Compare(T? x, T? y) + { + if (sortProperties.Count <= 0) + { + Type listType = source.GetType(); + Type? type; + + if (listType.IsGenericType) + { + type = listType.GetGenericArguments()[0]; + } + else + { + type = x?.GetType(); + } + + foreach (SortDescription sd in sortDescriptions) + { + if (!string.IsNullOrEmpty(sd.PropertyName)) + { + sortProperties[sd.PropertyName] = type?.GetProperty(sd.PropertyName); + } + } + } + + foreach (SortDescription sd in sortDescriptions) + { + T? cx, cy; + + if (string.IsNullOrEmpty(sd.PropertyName)) + { + cx = x; + cy = y; + } + else + { + PropertyInfo? pi = sortProperties[sd.PropertyName]; + + cx = (T?)pi?.GetValue(x); + cy = (T?)pi?.GetValue(y); + } + + int cmp = sd.Comparer.Compare(cx, cy); + + if (cmp is not 0) + { + return sd.Direction is SortDirection.Ascending ? +cmp : -cmp; + } + } + + return 0; + } + + internal void OnPropertyChanged([CallerMemberName] string propertyName = default!) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void ItemOnPropertyChanged(object? item, PropertyChangedEventArgs e) + { + if (!liveShapingEnabled) + { + return; + } + + ArgumentNullException.ThrowIfNull(item); + T typedItem = (T)item; + + bool? filterResult = filter?.Invoke(typedItem); + + if (filterResult.HasValue && observedFilterProperties.Contains(e.PropertyName)) + { + int viewIndex = view.IndexOf(typedItem); + if (viewIndex != -1 && !filterResult.Value) + { + RemoveFromView(viewIndex, typedItem); + } + else if (viewIndex == -1 && filterResult.Value) + { + int index = source.IndexOf(typedItem); + HandleItemAdded(index, typedItem); + } + } + + if ((filterResult ?? true) && SortDescriptions.Any(sd => sd.PropertyName == e.PropertyName)) + { + int oldIndex = view.IndexOf(typedItem); + + // Check if item is in view: + if (oldIndex < 0) + { + return; + } + + view.RemoveAt(oldIndex); + int targetIndex = view.BinarySearch(typedItem, this); + if (targetIndex < 0) + { + targetIndex = ~targetIndex; + } + + // Only trigger expensive UI updates if the index really changed: + if (targetIndex != oldIndex) + { + OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, oldIndex, typedItem)); + + view.Insert(targetIndex, typedItem); + + OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, targetIndex, typedItem)); + } + else + { + view.Insert(targetIndex, typedItem); + } + } + else if (string.IsNullOrEmpty(e.PropertyName)) + { + HandleSourceChanged(); + } + } + + private void AttachPropertyChangedHandler(IEnumerable items) + { + if (!liveShapingEnabled || items is null) + { + return; + } + + foreach (object item in items) + { + if (item is INotifyPropertyChanged notifyPropertyChanged) + { + notifyPropertyChanged.PropertyChanged += ItemOnPropertyChanged; + } + } + } + + private void DetachPropertyChangedHandler(IEnumerable items) + { + if (!liveShapingEnabled || items is null) + { + return; + } + + foreach (object item in items) + { + if (item is INotifyPropertyChanged notifyPropertyChanged) + { + notifyPropertyChanged.PropertyChanged -= ItemOnPropertyChanged; + } + } + } + + private void HandleSortChanged() + { + sortProperties.Clear(); + view.Sort(this); + sortProperties.Clear(); + OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset)); + } + + private void HandleFilterChanged() + { + if (filter is not null) + { + for (int index = 0; index < view.Count; index++) + { + T item = view[index]; + if (filter(item)) + { + continue; + } + + RemoveFromView(index, item); + index--; + } + } + + HashSet viewHash = new(view); + int viewIndex = 0; + for (int index = 0; index < source.Count; index++) + { + T item = source[index]; + if (viewHash.Contains(item)) + { + viewIndex++; + continue; + } + + if (HandleItemAdded(index, item, viewIndex)) + { + viewIndex++; + } + } + } + + private void HandleSourceChanged() + { + sortProperties.Clear(); + T? currentItem = CurrentItem; + view.Clear(); + foreach (T item in Source) + { + if (filter is not null && !filter(item)) + { + continue; + } + + if (sortDescriptions.Count > 0) + { + int targetIndex = view.BinarySearch(item, this); + if (targetIndex < 0) + { + targetIndex = ~targetIndex; + } + + view.Insert(targetIndex, item); + } + else + { + view.Add(item); + } + } + + sortProperties.Clear(); + OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset)); + MoveCurrentTo(currentItem); + } + + private void SourceNotifyCollectionChangedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + ArgumentNullException.ThrowIfNull(e.NewItems); + AttachPropertyChangedHandler(e.NewItems); + if (deferCounter <= 0) + { + if (e.NewItems?.Count == 1) + { + object? newItem = e.NewItems[0]; + ArgumentNullException.ThrowIfNull(newItem); + HandleItemAdded(e.NewStartingIndex, (T)newItem); + } + else + { + HandleSourceChanged(); + } + } + + break; + case NotifyCollectionChangedAction.Remove: + ArgumentNullException.ThrowIfNull(e.OldItems); + DetachPropertyChangedHandler(e.OldItems); + if (deferCounter <= 0) + { + if (e.OldItems?.Count == 1) + { + object? oldItem = e.OldItems[0]; + ArgumentNullException.ThrowIfNull(oldItem); + HandleItemRemoved(e.OldStartingIndex, (T)oldItem); + } + else + { + HandleSourceChanged(); + } + } + + break; + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Reset: + if (deferCounter <= 0) + { + HandleSourceChanged(); + } + + break; + } + } + + private bool HandleItemAdded(int newStartingIndex, T newItem, int? viewIndex = null) + { + if (filter is not null && !filter(newItem)) + { + return false; + } + + int newViewIndex = view.Count; + + if (sortDescriptions.Count > 0) + { + sortProperties.Clear(); + newViewIndex = view.BinarySearch(newItem, this); + if (newViewIndex < 0) + { + newViewIndex = ~newViewIndex; + } + } + else if (filter is not null) + { + if (source is null) + { + HandleSourceChanged(); + return false; + } + + if (newStartingIndex == 0 || view.Count == 0) + { + newViewIndex = 0; + } + else if (newStartingIndex == source.Count - 1) + { + newViewIndex = view.Count; + } + else if (viewIndex.HasValue) + { + newViewIndex = viewIndex.Value; + } + else + { + for (int i = 0, j = 0; i < source.Count; i++) + { + if (i == newStartingIndex) + { + newViewIndex = j; + break; + } + + if (Equals(view[j], source[i])) + { + j++; + } + } + } + } + + view.Insert(newViewIndex, newItem); + if (newViewIndex <= CurrentPosition) + { + CurrentPosition++; + } + + OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemInserted, newViewIndex, newItem)); + return true; + } + + private void HandleItemRemoved(int oldStartingIndex, T oldItem) + { + if (filter is not null && !filter(oldItem)) + { + return; + } + + if (oldStartingIndex < 0 || oldStartingIndex >= view.Count || !Equals(view[oldStartingIndex], oldItem)) + { + oldStartingIndex = view.IndexOf(oldItem); + } + + if (oldStartingIndex < 0) + { + return; + } + + RemoveFromView(oldStartingIndex, oldItem); + } + + private void RemoveFromView(int itemIndex, T item) + { + view.RemoveAt(itemIndex); + if (itemIndex <= CurrentPosition) + { + CurrentPosition--; + } + + OnVectorChanged(new VectorChangedEventArgs(CollectionChange.ItemRemoved, itemIndex, item)); + } + + private void SortDescriptionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (deferCounter > 0) + { + return; + } + + HandleSortChanged(); + } + + private bool MoveCurrentToIndex(int i) + { + if (i < -1 || i >= view.Count) + { + return false; + } + + if (i == CurrentPosition) + { + return false; + } + + CurrentChangingEventArgs e = new(); + OnCurrentChanging(e); + if (e.Cancel) + { + return false; + } + + CurrentPosition = i; + OnCurrentChanged(default!); + return true; + } + + private void OnCurrentChanging(CurrentChangingEventArgs e) + { + if (deferCounter > 0) + { + return; + } + + CurrentChanging?.Invoke(this, e); + } + + private void OnCurrentChanged(object e) + { + if (deferCounter > 0) + { + return; + } + + CurrentChanged?.Invoke(this, e); + OnPropertyChanged(nameof(CurrentItem)); + } + + private void OnVectorChanged(IVectorChangedEventArgs e) + { + if (deferCounter > 0) + { + return; + } + + VectorChanged?.Invoke(this, e); + OnPropertyChanged(nameof(Count)); + } + + internal sealed class NotificationDeferrer : IDisposable + { + private readonly AdvancedCollectionView advancedCollectionView; + private readonly T? currentItem; + + public NotificationDeferrer(AdvancedCollectionView acvs) + { + advancedCollectionView = acvs; + currentItem = advancedCollectionView.CurrentItem; + advancedCollectionView.deferCounter++; + } + + public void Dispose() + { + advancedCollectionView.MoveCurrentTo(currentItem); + advancedCollectionView.deferCounter--; + advancedCollectionView.Refresh(); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/IAdvancedCollectionView.cs b/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/IAdvancedCollectionView.cs new file mode 100644 index 00000000..e8fd0810 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/IAdvancedCollectionView.cs @@ -0,0 +1,105 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using CommunityToolkit.WinUI.Collections; +using Microsoft.UI.Xaml.Data; +using System.Collections; + +namespace Snap.Hutao.Control.Collection.AdvancedCollectionView; + +internal interface IAdvancedCollectionView : ICollectionView, IEnumerable + where T : class +{ + bool CanFilter { get; } + + bool CanSort { get; } + + object? ICollectionView.CurrentItem + { + get => CurrentItem; + } + + new T? CurrentItem { get; } + + Predicate? Filter { get; set; } + + IList SortDescriptions { get; } + + IEnumerable SourceCollection { get; } + + object IList.this[int index] + { + get => this[index]; + set => this[index] = (T)value; + } + + new T this[int index] { get; set; } + + void ICollection.Add(object item) + { + Add((T)item); + } + + void Add(T item); + + void ClearObservedFilterProperties(); + + bool ICollection.Contains(object item) + { + return Contains((T)item); + } + + bool Contains(T item); + + void ICollection.CopyTo(object[] array, int arrayIndex) + { + CopyTo((T[])array, arrayIndex); + } + + void CopyTo(T[] array, int arrayIndex); + + IDisposable DeferRefresh(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + new IEnumerator GetEnumerator(); + + int IList.IndexOf(object item) + { + return IndexOf((T)item); + } + + int IndexOf(T item); + + void IList.Insert(int index, object item) + { + Insert(index, (T)item); + } + + void Insert(int index, T item); + + bool ICollectionView.MoveCurrentTo(object item) + { + return MoveCurrentTo((T)item); + } + + bool MoveCurrentTo(T item); + + void ObserveFilterProperty(string propertyName); + + void Refresh(); + + void RefreshFilter(); + + void RefreshSorting(); + + bool ICollection.Remove(object item) + { + return Remove((T)item); + } + + bool Remove(T item); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/VectorChangedEventArgs.cs b/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/VectorChangedEventArgs.cs new file mode 100644 index 00000000..d0e1a439 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Collection/AdvancedCollectionView/VectorChangedEventArgs.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.Foundation.Collections; + +namespace Snap.Hutao.Control.Collection.AdvancedCollectionView; + +internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs +{ + public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!) + { + CollectionChange = cc; + Index = (uint)index; + } + + public CollectionChange CollectionChange { get; } + + public uint Index { get; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs index 5689cc8b..263a6378 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ExceptionService/ThrowHelper.cs @@ -73,6 +73,15 @@ internal static class ThrowHelper throw new NotSupportedException(message); } + [MethodImpl(MethodImplOptions.NoInlining)] + public static void NotSupportedIf(bool condition, string message) + { + if (condition) + { + throw new NotSupportedException(message); + } + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static OperationCanceledException OperationCanceled(string message, Exception? inner = default) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs index 8c3b69ac..4c1891e4 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Achievement/AchievementViewModel.cs @@ -1,8 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Collections; using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Control.Collection.AdvancedCollectionView; using Snap.Hutao.Core.IO; using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Factory.ContentDialog; @@ -18,6 +18,8 @@ using System.Text.RegularExpressions; using EntityAchievementArchive = Snap.Hutao.Model.Entity.AchievementArchive; using MetadataAchievement = Snap.Hutao.Model.Metadata.Achievement.Achievement; using MetadataAchievementGoal = Snap.Hutao.Model.Metadata.Achievement.AchievementGoal; +using SortDescription = CommunityToolkit.WinUI.Collections.SortDescription; +using SortDirection = CommunityToolkit.WinUI.Collections.SortDirection; namespace Snap.Hutao.ViewModel.Achievement; @@ -41,7 +43,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav private readonly JsonSerializerOptions options; private readonly ITaskContext taskContext; - private AdvancedCollectionView? achievements; + private AdvancedCollectionView? achievements; private List? achievementGoals; private AchievementGoalView? selectedAchievementGoal; private ObservableCollection? archives; @@ -83,7 +85,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav /// /// 成就视图 /// - public AdvancedCollectionView? Achievements + public AdvancedCollectionView? Achievements { get => achievements; set => SetProperty(ref achievements, value); @@ -340,17 +342,19 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav [Command("SortUncompletedSwitchCommand")] private void UpdateAchievementsSort() { - if (Achievements is not null) + if (Achievements is null) { - if (IsUncompletedItemsFirst) - { - Achievements.SortDescriptions.Add(uncompletedItemsFirstSortDescription); - Achievements.SortDescriptions.Add(completionTimeSortDescription); - } - else - { - Achievements.SortDescriptions.Clear(); - } + return; + } + + if (IsUncompletedItemsFirst) + { + Achievements.SortDescriptions.Add(uncompletedItemsFirstSortDescription); + Achievements.SortDescriptions.Add(completionTimeSortDescription); + } + else + { + Achievements.SortDescriptions.Clear(); } } @@ -365,7 +369,7 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav else { Model.Primitive.AchievementGoalId goalId = goal.Id; - Achievements.Filter = (object o) => o is AchievementView view && view.Inner.Goal == goalId; + Achievements.Filter = (AchievementView view) => view.Inner.Goal == goalId; } } } @@ -381,19 +385,18 @@ internal sealed partial class AchievementViewModel : Abstraction.ViewModel, INav { if (uint.TryParse(search, out uint achievementId)) { - Achievements.Filter = obj => ((AchievementView)obj).Inner.Id == achievementId; + Achievements.Filter = view => view.Inner.Id == achievementId; return; } if (VersionRegex().IsMatch(search)) { - Achievements.Filter = obj => ((AchievementView)obj).Inner.Version == search; + Achievements.Filter = view => view.Inner.Version == search; return; } - Achievements.Filter = obj => + Achievements.Filter = view => { - AchievementView view = (AchievementView)obj; return view.Inner.Title.Contains(search, StringComparison.CurrentCultureIgnoreCase) || view.Inner.Description.Contains(search, StringComparison.CurrentCultureIgnoreCase); }; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs index d3a52c75..07269a06 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs @@ -15,13 +15,13 @@ internal sealed class GameAccountFilter this.type = type; } - public bool Filter(object? item) + public bool Filter(GameAccount? item) { if (type is null) { return true; } - return item is GameAccount account && account.Type == type; + return item is not null && item.Type == type; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index db52d5dc..6aeb6596 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -1,9 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Collections; using Microsoft.Extensions.Caching.Memory; using Microsoft.UI.Windowing; +using Snap.Hutao.Control.Collection.AdvancedCollectionView; using Snap.Hutao.Core; using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Diagnostics.CodeAnalysis; @@ -50,7 +50,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView private readonly AppOptions appOptions; private LaunchScheme? selectedScheme; - private AdvancedCollectionView? gameAccountsView; + private AdvancedCollectionView? gameAccountsView; private GameAccount? selectedGameAccount; private GameResource? gameResource; private bool gamePathSelectedAndValid; @@ -75,7 +75,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView set => SetSelectedSchemeAsync(value).SafeForget(); } - public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } + public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } @@ -130,9 +130,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView ArgumentNullException.ThrowIfNull(GameAccountsView); // Exists in the source collection - if (GameAccountsView.SourceCollection.Cast().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount) + if (GameAccountsView.SourceCollection.FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount) { - SelectedGameAccount = GameAccountsView.Cast().FirstOrDefault(g => g.AttachUid == uid); + SelectedGameAccount = GameAccountsView.View.FirstOrDefault(g => g.AttachUid == uid); // But not exists in the view for current scheme if (SelectedGameAccount is null) @@ -241,8 +241,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView { await taskContext.SwitchToMainThreadAsync(); SelectedGameAccount = account; - - await UpdateGameAccountsViewAsync().ConfigureAwait(false); } } catch (UserdataCorruptedException ex) @@ -328,6 +326,18 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView GameResource = response.Data; } } + + async ValueTask UpdateGameAccountsViewAsync() + { + gameAccountFilter = new(SelectedScheme?.GetSchemeType()); + ObservableReorderableDbCollection accounts = gameService.GameAccountCollection; + + await taskContext.SwitchToMainThreadAsync(); + GameAccountsView = new(accounts, true) + { + Filter = gameAccountFilter.Filter, + }; + } } [Command("IdentifyMonitorsCommand")] @@ -353,16 +363,4 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView window.Close(); } } - - private async ValueTask UpdateGameAccountsViewAsync() - { - gameAccountFilter = new(SelectedScheme?.GetSchemeType()); - ObservableReorderableDbCollection accounts = gameService.GameAccountCollection; - - await taskContext.SwitchToMainThreadAsync(); - GameAccountsView = new(accounts, true) - { - Filter = gameAccountFilter.Filter, - }; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index a7d29315..300a77c3 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Collections; +using Snap.Hutao.Control.Collection.AdvancedCollectionView; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game; @@ -27,13 +27,13 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli private readonly IGameServiceFacade gameService; private readonly ITaskContext taskContext; - private AdvancedCollectionView? gameAccountsView; + private AdvancedCollectionView? gameAccountsView; private GameAccount? selectedGameAccount; private GameAccountFilter? gameAccountFilter; public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; } - public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } + public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } /// /// 选中的账号 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/AvatarFilter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/AvatarFilter.cs index 5c6ac72b..2d765c25 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/AvatarFilter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/AvatarFilter.cs @@ -17,9 +17,9 @@ internal static class AvatarFilter /// /// 输入 /// 筛选操作 - public static Predicate Compile(string input) + public static Predicate Compile(string input) { - return (object o) => o is Avatar avatar && DoFilter(input, avatar); + return (Avatar avatar) => DoFilter(input, avatar); } private static bool DoFilter(string input, Avatar avatar) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs index 0e1540f9..06731581 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs @@ -17,9 +17,9 @@ internal static class WeaponFilter /// /// 输入 /// 筛选操作 - public static Predicate Compile(string input) + public static Predicate Compile(string input) { - return (object o) => o is Weapon weapon && DoFilter(input, weapon); + return (Weapon weapon) => DoFilter(input, weapon); } private static bool DoFilter(string input, Weapon weapon) diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index e0b4bad6..250197cb 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Collections; +using Snap.Hutao.Control.Collection.AdvancedCollectionView; using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; @@ -43,7 +43,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel private readonly CalculateClient calculateClient; private readonly IUserService userService; - private AdvancedCollectionView? avatars; + private AdvancedCollectionView? avatars; private Avatar? selected; private string? filterText; private BaseValueInfo? baseValueInfo; @@ -53,7 +53,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel /// /// 角色列表 /// - public AdvancedCollectionView? Avatars { get => avatars; set => SetProperty(ref avatars, value); } + public AdvancedCollectionView? Avatars { get => avatars; set => SetProperty(ref avatars, value); } /// /// 选中的角色 @@ -99,8 +99,8 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel await CombineComplexDataAsync(list, idMaterialMap).ConfigureAwait(false); await taskContext.SwitchToMainThreadAsync(); - Avatars = new AdvancedCollectionView(list, true); - Selected = Avatars.Cast().FirstOrDefault(); + Avatars = new(list, true); + Selected = Avatars.View.ElementAtOrDefault(0); return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs index cd42acc1..69ee0bbd 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiMonsterViewModel.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Collections; +using Snap.Hutao.Control.Collection.AdvancedCollectionView; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Item; using Snap.Hutao.Model.Metadata.Monster; @@ -20,7 +20,7 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel private readonly IMetadataService metadataService; private readonly ITaskContext taskContext; - private AdvancedCollectionView? monsters; + private AdvancedCollectionView? monsters; private Monster? selected; private BaseValueInfo? baseValueInfo; private Dictionary>? levelMonsterCurveMap; @@ -28,7 +28,7 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel /// /// 角色列表 /// - public AdvancedCollectionView? Monsters { get => monsters; set => SetProperty(ref monsters, value); } + public AdvancedCollectionView? Monsters { get => monsters; set => SetProperty(ref monsters, value); } /// /// 选中的角色 @@ -65,8 +65,8 @@ internal sealed partial class WikiMonsterViewModel : Abstraction.ViewModel List ordered = monsters.SortBy(m => m.RelationshipId.Value); await taskContext.SwitchToMainThreadAsync(); - Monsters = new AdvancedCollectionView(ordered, true); - Selected = Monsters.Cast().FirstOrDefault(); + Monsters = new(ordered, true); + Selected = Monsters.View.ElementAtOrDefault(0); return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index d64196c8..37615619 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.WinUI.Collections; +using Snap.Hutao.Control.Collection.AdvancedCollectionView; using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Calculable; using Snap.Hutao.Model.Entity.Primitive; @@ -40,7 +40,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel private readonly IInfoBarService infoBarService; private readonly IUserService userService; - private AdvancedCollectionView? weapons; + private AdvancedCollectionView? weapons; private Weapon? selected; private string? filterText; private BaseValueInfo? baseValueInfo; @@ -50,7 +50,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel /// /// 角色列表 /// - public AdvancedCollectionView? Weapons { get => weapons; set => SetProperty(ref weapons, value); } + public AdvancedCollectionView? Weapons { get => weapons; set => SetProperty(ref weapons, value); } /// /// 选中的角色 @@ -96,8 +96,8 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel await taskContext.SwitchToMainThreadAsync(); - Weapons = new AdvancedCollectionView(list, true); - Selected = Weapons.Cast().FirstOrDefault(); + Weapons = new(list, true); + Selected = Weapons.View.ElementAtOrDefault(0); } }