diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AssociationTypeIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AssociationTypeIconConverter.cs new file mode 100644 index 00000000..93321a30 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/AssociationTypeIconConverter.cs @@ -0,0 +1,54 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Control; +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Model.Metadata.Converter; + +internal sealed class AssociationTypeIconConverter : ValueConverter +{ + private static readonly Dictionary LocalizedNameToAssociationType = new() + { + [SH.ModelIntrinsicAssociationTypeMondstadt] = AssociationType.ASSOC_TYPE_MONDSTADT, + [SH.ModelIntrinsicAssociationTypeLiyue] = AssociationType.ASSOC_TYPE_LIYUE, + [SH.ModelIntrinsicAssociationTypeFatui] = AssociationType.ASSOC_TYPE_FATUI, + [SH.ModelIntrinsicAssociationTypeInazuma] = AssociationType.ASSOC_TYPE_INAZUMA, + [SH.ModelIntrinsicAssociationTypeRanger] = AssociationType.ASSOC_TYPE_RANGER, + [SH.ModelIntrinsicAssociationTypeSumeru] = AssociationType.ASSOC_TYPE_SUMERU, + [SH.ModelIntrinsicAssociationTypeFontaine] = AssociationType.ASSOC_TYPE_FONTAINE, + [SH.ModelIntrinsicAssociationTypeNatlan] = AssociationType.ASSOC_TYPE_NATLAN, + [SH.ModelIntrinsicAssociationTypeSnezhnaya] = AssociationType.ASSOC_TYPE_SNEZHNAYA, + }; + + public static Uri? AssociationTypeNameToIconUri(string associationTypeName) + { + return AssociationTypeToIconUri(LocalizedNameToAssociationType.GetValueOrDefault(associationTypeName)); + } + + public static Uri? AssociationTypeToIconUri(AssociationType type) + { + string? association = type switch + { + AssociationType.ASSOC_TYPE_MONDSTADT => "Mengde", + AssociationType.ASSOC_TYPE_LIYUE => "Liyue", + AssociationType.ASSOC_TYPE_FATUI => null, + AssociationType.ASSOC_TYPE_INAZUMA => "Inazuma", + AssociationType.ASSOC_TYPE_RANGER => null, + AssociationType.ASSOC_TYPE_SUMERU => "Sumeru", + AssociationType.ASSOC_TYPE_FONTAINE => "Fontaine", + AssociationType.ASSOC_TYPE_NATLAN => null, + AssociationType.ASSOC_TYPE_SNEZHNAYA => null, + _ => throw Must.NeverHappen(), + }; + + return association is null + ? null + : Web.HutaoEndpoints.StaticRaw("ChapterIcon", $"UI_ChapterIcon_{association}.png").ToUri(); + } + + public override Uri? Convert(AssociationType from) + { + return AssociationTypeToIconUri(from); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs index 20df90d1..c3da4da8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/QualityColorConverter.cs @@ -15,10 +15,24 @@ namespace Snap.Hutao.Model.Metadata.Converter; [HighQuality] internal sealed class QualityColorConverter : ValueConverter { - /// - public override Color Convert(QualityType from) + private static readonly Dictionary LocalizedNameToQualityType = new() { - return from switch + [SH.ModelIntrinsicItemQualityWhite] = QualityType.QUALITY_WHITE, + [SH.ModelIntrinsicItemQualityGreen] = QualityType.QUALITY_GREEN, + [SH.ModelIntrinsicItemQualityBlue] = QualityType.QUALITY_BLUE, + [SH.ModelIntrinsicItemQualityPurple] = QualityType.QUALITY_PURPLE, + [SH.ModelIntrinsicItemQualityOrange] = QualityType.QUALITY_ORANGE, + [SH.ModelIntrinsicItemQualityRed] = QualityType.QUALITY_ORANGE_SP, + }; + + public static Color QualityNameToColor(string qualityName) + { + return QualityToColor(LocalizedNameToQualityType.GetValueOrDefault(qualityName)); + } + + public static Color QualityToColor(QualityType quality) + { + return quality switch { QualityType.QUALITY_WHITE => StructMarshal.Color(0xFF72778B), QualityType.QUALITY_GREEN => StructMarshal.Color(0xFF2A8F72), @@ -28,4 +42,10 @@ internal sealed class QualityColorConverter : ValueConverter _ => Colors.Transparent, }; } + + /// + public override Color Convert(QualityType from) + { + return QualityToColor(from); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs index e6f34fe4..41712715 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/WeaponTypeIconConverter.cs @@ -12,6 +12,20 @@ namespace Snap.Hutao.Model.Metadata.Converter; [HighQuality] internal sealed class WeaponTypeIconConverter : ValueConverter { + private static readonly Dictionary LocalizedNameToWeaponType = new() + { + [SH.ModelIntrinsicWeaponTypeSwordOneHand] = WeaponType.WEAPON_SWORD_ONE_HAND, + [SH.ModelIntrinsicWeaponTypeBow] = WeaponType.WEAPON_BOW, + [SH.ModelIntrinsicWeaponTypePole] = WeaponType.WEAPON_POLE, + [SH.ModelIntrinsicWeaponTypeClaymore] = WeaponType.WEAPON_CLAYMORE, + [SH.ModelIntrinsicWeaponTypeCatalyst] = WeaponType.WEAPON_CATALYST, + }; + + public static Uri WeaponTypeNameToIconUri(string weaponTypeName) + { + return WeaponTypeToIconUri(LocalizedNameToWeaponType.GetValueOrDefault(weaponTypeName)); + } + /// /// 将武器类型转换为图标链接 /// diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml index e3cab592..43c5d46e 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/WikiAvatarPage.xaml @@ -72,8 +72,45 @@ - - + + + + + + + + + + + + + + + @@ -270,18 +307,18 @@ + TokenItemTemplate="{StaticResource TokenTemplate}"/> - - + + + + + + + + + + + + + + + @@ -137,19 +174,18 @@ + TokenItemTemplate="{StaticResource TokenTemplate}"/> /// 输入 /// 筛选操作 - public static Predicate Compile(string input) + public static Predicate Compile(ObservableCollection input) { return (Avatar avatar) => DoFilter(input, avatar); } - private static bool DoFilter(string input, Avatar avatar) + private static bool DoFilter(ObservableCollection input, Avatar avatar) { List matches = []; - foreach (StringSegment segment in new StringTokenizer(input, [' '])) + + foreach (IGrouping tokens in input.GroupBy(token => token.Kind, token => token.Value)) { - string value = segment.ToString(); - - if (avatar.Name == value) + switch (tokens.Key) { - matches.Add(true); - continue; - } + case SearchTokenKind.ElementNames: + if (IntrinsicFrozen.ElementNames.Overlaps(tokens)) + { + matches.Add(tokens.Contains(avatar.FetterInfo.VisionBefore)); + } - if (IntrinsicFrozen.ElementNames.Contains(value)) - { - matches.Add(avatar.FetterInfo.VisionBefore == value); - continue; - } + break; + case SearchTokenKind.AssociationTypes: + if (IntrinsicFrozen.AssociationTypes.Overlaps(tokens)) + { + matches.Add(tokens.Contains(avatar.FetterInfo.Association.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.AssociationTypes.Contains(value)) - { - matches.Add(avatar.FetterInfo.Association.GetLocalizedDescriptionOrDefault() == value); - continue; - } + break; + case SearchTokenKind.WeaponTypes: + if (IntrinsicFrozen.WeaponTypes.Overlaps(tokens)) + { + matches.Add(tokens.Contains(avatar.Weapon.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.WeaponTypes.Contains(value)) - { - matches.Add(avatar.Weapon.GetLocalizedDescriptionOrDefault() == value); - continue; - } + break; + case SearchTokenKind.ItemQualities: + if (IntrinsicFrozen.ItemQualities.Overlaps(tokens)) + { + matches.Add(tokens.Contains(avatar.Quality.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.ItemQualities.Contains(value)) - { - matches.Add(avatar.Quality.GetLocalizedDescriptionOrDefault() == value); - continue; - } + break; + case SearchTokenKind.BodyTypes: + if (IntrinsicFrozen.BodyTypes.Overlaps(tokens)) + { + matches.Add(tokens.Contains(avatar.Body.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.BodyTypes.Contains(value)) - { - matches.Add(avatar.Body.GetLocalizedDescriptionOrDefault() == value); - continue; + break; + case SearchTokenKind.Other: + matches.Add(tokens.Contains(avatar.Name)); + break; } - - matches.Add(false); } return matches.Count > 0 && matches.Aggregate((a, b) => a && b); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/SearchToken.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/SearchToken.cs new file mode 100644 index 00000000..cb7617a2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/SearchToken.cs @@ -0,0 +1,118 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic.Frozen; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Metadata.Weapon; +using Windows.UI; + +namespace Snap.Hutao.ViewModel.Wiki; + +internal class SearchToken +{ + private SearchTokenKind kind; + private bool isKindInitialized; + private object isKindInitializedLock = new(); + + public SearchToken(string value) + { + Value = value; + } + + public SearchToken(Avatar avatar) + { + Value = avatar.Name; + SideIconUri = AvatarSideIconConverter.IconNameToUri(avatar.SideIcon); + } + + public SearchToken(Weapon weapon) + { + Value = weapon.Name; + SideIconUri = EquipIconConverter.IconNameToUri(weapon.Icon); + } + + public string Value { get; } + + public Uri? SideIconUri { get; } + + public Uri? IconUri + { + get => Kind switch + { + SearchTokenKind.AssociationTypes => AssociationTypeIconConverter.AssociationTypeNameToIconUri(Value), + SearchTokenKind.ElementNames => ElementNameIconConverter.ElementNameToIconUri(Value), + SearchTokenKind.WeaponTypes => WeaponTypeIconConverter.WeaponTypeNameToIconUri(Value), + _ => null, + }; + } + + public Color? Quality + { + get => Kind switch + { + SearchTokenKind.ItemQualities => QualityColorConverter.QualityNameToColor(Value), + _ => null, + }; + } + + public SearchTokenKind Kind + { + get + { + return LazyInitializer.EnsureInitialized(ref kind, ref isKindInitialized, ref isKindInitializedLock, GetKind); + + SearchTokenKind GetKind() + { + if (IntrinsicFrozen.AssociationTypes.Contains(Value)) + { + return SearchTokenKind.AssociationTypes; + } + + if (IntrinsicFrozen.BodyTypes.Contains(Value)) + { + return SearchTokenKind.BodyTypes; + } + + if (IntrinsicFrozen.ElementNames.Contains(Value)) + { + return SearchTokenKind.ElementNames; + } + + if (IntrinsicFrozen.FightProperties.Contains(Value)) + { + return SearchTokenKind.FightProperties; + } + + if (IntrinsicFrozen.ItemQualities.Contains(Value)) + { + return SearchTokenKind.ItemQualities; + } + + if (IntrinsicFrozen.WeaponTypes.Contains(Value)) + { + return SearchTokenKind.WeaponTypes; + } + + return SearchTokenKind.Other; + } + } + } + + public override string ToString() + { + return Value; + } +} + +[SuppressMessage("", "SA1201")] +internal enum SearchTokenKind +{ + AssociationTypes, + BodyTypes, + ElementNames, + FightProperties, + ItemQualities, + Other, // Include avatar and weapon + WeaponTypes, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs index 06731581..392a6027 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WeaponFilter.cs @@ -1,9 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Microsoft.Extensions.Primitives; using Snap.Hutao.Model.Intrinsic.Frozen; using Snap.Hutao.Model.Metadata.Weapon; +using System.Collections.ObjectModel; namespace Snap.Hutao.ViewModel.Wiki; @@ -17,41 +17,43 @@ internal static class WeaponFilter /// /// 输入 /// 筛选操作 - public static Predicate Compile(string input) + public static Predicate Compile(ObservableCollection input) { return (Weapon weapon) => DoFilter(input, weapon); } - private static bool DoFilter(string input, Weapon weapon) + private static bool DoFilter(ObservableCollection input, Weapon weapon) { List matches = []; - foreach (StringSegment segment in new StringTokenizer(input, [' '])) + foreach (IGrouping tokens in input.GroupBy(token => token.Kind, token => token.Value)) { - string value = segment.ToString(); - - if (weapon.Name == value) + switch (tokens.Key) { - matches.Add(true); - continue; - } + case SearchTokenKind.WeaponTypes: + if (IntrinsicFrozen.WeaponTypes.Overlaps(tokens)) + { + matches.Add(tokens.Contains(weapon.WeaponType.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.WeaponTypes.Contains(value)) - { - matches.Add(weapon.WeaponType.GetLocalizedDescriptionOrDefault() == value); - continue; - } + break; + case SearchTokenKind.ItemQualities: + if (IntrinsicFrozen.ItemQualities.Overlaps(tokens)) + { + matches.Add(tokens.Contains(weapon.Quality.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.ItemQualities.Contains(value)) - { - matches.Add(weapon.Quality.GetLocalizedDescriptionOrDefault() == value); - continue; - } + break; + case SearchTokenKind.FightProperties: + if (IntrinsicFrozen.FightProperties.Overlaps(tokens)) + { + matches.Add(tokens.Contains(weapon.GrowCurves.ElementAtOrDefault(1)?.Type.GetLocalizedDescriptionOrDefault())); + } - if (IntrinsicFrozen.FightProperties.Contains(value)) - { - matches.Add(weapon.GrowCurves.ElementAtOrDefault(1)?.Type.GetLocalizedDescriptionOrDefault() == value); - continue; + break; + case SearchTokenKind.Other: + matches.Add(tokens.Contains(weapon.Name)); + break; } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs index 53f44fbf..1691c7ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiAvatarViewModel.cs @@ -50,7 +50,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel, IWiki private AdvancedCollectionView? avatars; private Avatar? selected; - private ObservableCollection? filterTokens; + private ObservableCollection? filterTokens; private string? filterToken; private BaseValueInfo? baseValueInfo; private Dictionary>? levelAvatarCurveMap; @@ -84,7 +84,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel, IWiki /// /// 保存的筛选标志 /// - public ObservableCollection? FilterTokens { get => filterTokens; set => SetProperty(ref filterTokens, value); } + public ObservableCollection? FilterTokens { get => filterTokens; set => SetProperty(ref filterTokens, value); } public string? FilterToken { get => filterToken; set => SetProperty(ref filterToken, value); } @@ -94,6 +94,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel, IWiki { accessor.TokenizingTextBox.TextChanged += OnFilterSuggestionRequested; accessor.TokenizingTextBox.QuerySubmitted += OnQuerySubmitted; + accessor.TokenizingTextBox.TokenItemAdding += OnTokenItemAdding; accessor.TokenizingTextBox.TokenItemAdded += OnTokenItemModified; accessor.TokenizingTextBox.TokenItemRemoved += OnTokenItemModified; } @@ -261,6 +262,27 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel, IWiki ApplyFilter(); } + private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args) + { + if (string.IsNullOrWhiteSpace(args.TokenText)) + { + return; + } + + if (Avatars is null) + { + return; + } + + if (Avatars.SourceCollection.SingleOrDefault(a => a.Name == args.TokenText) is { } avatar) + { + args.Item = new SearchToken(avatar); + return; + } + + args.Item = new SearchToken(args.TokenText); + } + private void OnTokenItemModified(TokenizingTextBox sender, object args) { ApplyFilter(); @@ -279,7 +301,7 @@ internal sealed partial class WikiAvatarViewModel : Abstraction.ViewModel, IWiki return; } - Avatars.Filter = AvatarFilter.Compile(string.Join(' ', FilterTokens)); + Avatars.Filter = AvatarFilter.Compile(FilterTokens); if (Selected is not null && Avatars.Contains(Selected)) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs index a1912c90..3d8a2a3d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Wiki/WikiWeaponViewModel.cs @@ -21,6 +21,7 @@ using Snap.Hutao.Service.User; using Snap.Hutao.View.Dialog; using Snap.Hutao.Web.Response; using System.Collections.Frozen; +using System.Collections.ObjectModel; using System.Runtime.InteropServices; using CalculateAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta; using CalculateClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient; @@ -46,7 +47,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel, IWiki private AdvancedCollectionView? weapons; private Weapon? selected; - private List? filterTokens; + private ObservableCollection? filterTokens; private string? filterToken; private BaseValueInfo? baseValueInfo; private Dictionary>? levelWeaponCurveMap; @@ -80,7 +81,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel, IWiki /// /// 保存的筛选标志 /// - public List? FilterTokens { get => filterTokens; set => SetProperty(ref filterTokens, value); } + public ObservableCollection? FilterTokens { get => filterTokens; set => SetProperty(ref filterTokens, value); } public string? FilterToken { get => filterToken; set => SetProperty(ref filterToken, value); } @@ -90,6 +91,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel, IWiki { accessor.TokenizingTextBox.TextChanged += OnFilterSuggestionRequested; accessor.TokenizingTextBox.QuerySubmitted += OnQuerySubmitted; + accessor.TokenizingTextBox.TokenItemAdding += OnTokenItemAdding; accessor.TokenizingTextBox.TokenItemAdded += OnTokenItemModified; accessor.TokenizingTextBox.TokenItemRemoved += OnTokenItemModified; } @@ -242,6 +244,27 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel, IWiki ApplyFilter(); } + private void OnTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args) + { + if (string.IsNullOrWhiteSpace(args.TokenText)) + { + return; + } + + if (Weapons is null) + { + return; + } + + if (Weapons.SourceCollection.SingleOrDefault(w => w.Name == args.TokenText) is { } weapon) + { + args.Item = new SearchToken(weapon); + return; + } + + args.Item = new SearchToken(args.TokenText); + } + private void OnTokenItemModified(TokenizingTextBox sender, object args) { ApplyFilter(); @@ -260,7 +283,7 @@ internal sealed partial class WikiWeaponViewModel : Abstraction.ViewModel, IWiki return; } - Weapons.Filter = WeaponFilter.Compile(string.Join(' ', FilterTokens)); + Weapons.Filter = WeaponFilter.Compile(FilterTokens); if (Selected is not null && Weapons.Contains(Selected)) {