Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/Dapr/GameServer.Host/_Imports.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@using System.Net.Http
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
Expand All @@ -15,7 +15,6 @@
@using Blazored.Modal.Services
@using Blazored.Toast
@using Blazored.Toast.Services
@using Blazored.Typeahead

@using BlazorInputFile

Expand Down
20 changes: 19 additions & 1 deletion src/DataModel/Configuration/Items/IncreasableItemOption.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// <copyright file="IncreasableItemOption.cs" company="MUnique">
// <copyright file="IncreasableItemOption.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Configuration.Items;

using System.Linq;
using MUnique.OpenMU.Annotations;

/// <summary>
Expand Down Expand Up @@ -54,4 +55,21 @@ public partial class IncreasableItemOption : ItemOption
/// </summary>
[MemberOfAggregate]
public virtual ICollection<ItemOptionOfLevel> LevelDependentOptions { get; protected set; } = null!;

/// <inheritdoc />
public override string ToString()
{
if (this.PowerUpDefinition != null)
{
return base.ToString();
}

var firstLevelOption = this.LevelDependentOptions?.OrderBy(l => l.Level).FirstOrDefault();
if (firstLevelOption?.PowerUpDefinition != null)
{
return $"{this.OptionType}: {firstLevelOption.PowerUpDefinition} ({this.Number})";
}

return base.ToString();
}
}
1 change: 0 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
<PackageVersion Include="BlazorInputFile" Version="0.2.0" />
<PackageVersion Include="Blazored.Modal" Version="7.3.1" />
<PackageVersion Include="Blazored.Toast" Version="4.2.1" />
<PackageVersion Include="Blazored.Typeahead" Version="4.7.0" />
<PackageVersion Include="BuildWebCompiler2022" Version="1.14.15" />
<PackageVersion Include="DG.AdvancedDataGridView" Version="1.2.30115.18" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.16.1" />
Expand Down
4 changes: 2 additions & 2 deletions src/Persistence/EntityFramework/TypedContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="TypedContext.cs" company="MUnique">
// <copyright file="TypedContext.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

Expand Down Expand Up @@ -41,7 +41,7 @@ public TypedContext(Type editType)
}

/// <inheritdoc/>
public IEntityType RootType => this._rootType ??= this.Model.GetEntityTypes().First(t => t.ClrType.BaseType == this.EditType);
public IEntityType RootType => this._rootType ??= this.Model.GetEntityTypes().First(t => t.ClrType == this.EditType || t.ClrType.BaseType == this.EditType);

/// <summary>
/// Gets the type which is edited with this context.
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/MUnique.OpenMU.Persistence.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
Expand Down
20 changes: 18 additions & 2 deletions src/PlugIns/PlugInConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="PlugInConfiguration.cs" company="MUnique">
// <copyright file="PlugInConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

Expand Down Expand Up @@ -81,7 +81,7 @@ public string Name
get
{
var plugInType = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.DefinedTypes)
.SelectMany(GetTypesSafely)
.FirstOrDefault(t => t.GUID == this.TypeId);
var plugInAttribute = plugInType?.GetCustomAttribute<DisplayAttribute>(inherit: false);

Expand All @@ -103,4 +103,20 @@ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

private static IEnumerable<TypeInfo> GetTypesSafely(Assembly assembly)
{
try
{
return assembly.DefinedTypes;
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t != null).Select(t => t!.GetTypeInfo());
}
catch
{
return Enumerable.Empty<TypeInfo>();
}
}
}
45 changes: 45 additions & 0 deletions src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@using MUnique.OpenMU.Web.AdminPanel.Properties

<div class="configuration-search">
<div class="input-group configuration-search-container">
<div class="input-group-prepend">
<span class="input-group-text">
<span class="oi oi-magnifying-glass" aria-hidden="true"></span>
</span>
</div>
<input type="search"
class="form-control configuration-search__input"
placeholder="@($"{Resources.Search}...")"
@bind-value="this._searchText"
@bind-value:event="oninput"
@bind-value:after="this.OnSearchInputAsync"
title="@Resources.Search"
@onfocus="this.OnSearchFocus"
@onblur="this.OnSearchBlurAsync"
@onkeydown="this.OnSearchKeyDownAsync"
autocomplete="off" />
@if (this._isLoading)
{
<div class="input-group-append">
<span class="input-group-text">
<div class="spinner-border spinner-border-sm text-muted" role="status"></div>
</span>
</div>
}
</div>
@if (this._searchResults.Count > 0 && !string.IsNullOrWhiteSpace(this._searchText))
{
<div class="dropdown-menu configuration-search__results show">
@foreach (var result in this._searchResults)
{
<button type="button"
class="dropdown-item configuration-search__result"
@onmousedown="@(() => this.NavigateToResult(result))"
@onmousedown:preventDefault="true">
<span class="configuration-search__result-caption">@result.Caption</span>
<span class="configuration-search__result-path">@result.Path</span>
</button>
}
</div>
}
</div>
215 changes: 215 additions & 0 deletions src/Web/AdminPanel/Components/Layout/ConfigurationSearch.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// <copyright file="ConfigurationSearch.razor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Web.AdminPanel.Components.Layout;

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MUnique.OpenMU.Web.AdminPanel.Services;
using MUnique.OpenMU.Web.Shared.Components;
using MUnique.OpenMU.Web.Shared.Services;

/// <summary>
/// Header search for configuration properties.
/// </summary>
public partial class ConfigurationSearch : IDisposable
{
private const int MinimumSearchLength = 2;
private const int MaximumResults = 15;

private readonly Debouncer _searchDebouncer = new(200);
private readonly List<ConfigurationSearchEntry> _searchResults = new();

private bool _isLoading;
private string _searchText = string.Empty;
private IReadOnlyList<ConfigurationSearchEntry> _searchEntries = Array.Empty<ConfigurationSearchEntry>();

[Inject]
private ConfigurationSearchIndexCache SearchIndexCache { get; set; } = null!;

[Inject]
private NavigationManager NavigationManager { get; set; } = null!;

[Inject]
private NavigationHistory NavigationHistory { get; set; } = null!;

[Inject]
private SetupService SetupService { get; set; } = null!;

/// <inheritdoc />
public void Dispose()
{
this.SetupService.DatabaseInitialized -= this.OnDatabaseInitializedAsync;
this._searchDebouncer.Dispose();
}

/// <inheritdoc />
protected override Task OnInitializedAsync()
{
this.SetupService.DatabaseInitialized += this.OnDatabaseInitializedAsync;

if (!this.RendererInfo.IsInteractive)
{
return base.OnInitializedAsync();
}

if (this.SearchIndexCache.IsLoaded)
{
this._searchEntries = this.SearchIndexCache.Entries;
}
else
{
this._isLoading = true;
_ = Task.Run(async () =>
{
try
{
await this.SearchIndexCache.EnsureLoadedAsync().ConfigureAwait(false);
}
catch
{
// Errors are logged inside EnsureLoadedAsync
}
finally
{
await this.InvokeAsync(() =>
{
this._searchEntries = this.SearchIndexCache.Entries;
this._isLoading = false;
this.StateHasChanged();
}).ConfigureAwait(false);
}
});
}

return base.OnInitializedAsync();
}

private static int CalculateScore(ConfigurationSearchEntry entry, string normalizedQuery, IReadOnlyList<string> queryParts)
{
if (queryParts.Count == 0 || !queryParts.All(part => entry.NormalizedHaystack.Contains(part, StringComparison.OrdinalIgnoreCase)))
{
return int.MaxValue;
}

var score = 100;
if (entry.NormalizedCaption.StartsWith(normalizedQuery, StringComparison.OrdinalIgnoreCase))
{
score -= 60;
}
else if (entry.NormalizedCaption.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase))
{
score -= 45;
}
else
{
// Caption does not contain the query, no score adjustment needed.
}

if (entry.NormalizedHaystack.StartsWith(normalizedQuery, StringComparison.OrdinalIgnoreCase))
{
score -= 20;
}

score += entry.Path.Length / 64;
return score;
}

private static string Normalize(string value)
{
return string.Join(
' ',
value.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}

private void OnSearchFocus(FocusEventArgs _)
{
this.UpdateSearchResults();
}

private Task OnSearchInputAsync()
{
_ = this._searchDebouncer.DebounceAsync(async token =>
{
if (!token.IsCancellationRequested)
{
await this.InvokeAsync(() =>
{
this.UpdateSearchResults();
this.StateHasChanged();
}).ConfigureAwait(false);
}
});

return Task.CompletedTask;
}

private async Task OnSearchBlurAsync(FocusEventArgs _)
{
await Task.Delay(100).ConfigureAwait(true);
this._searchResults.Clear();
}

private Task OnSearchKeyDownAsync(KeyboardEventArgs args)
{
if (string.Equals(args.Key, "Escape", StringComparison.Ordinal))
{
this._searchText = string.Empty;
this._searchResults.Clear();
}
else if (string.Equals(args.Key, "Enter", StringComparison.Ordinal)
&& this._searchResults.FirstOrDefault() is { } firstResult)
{
this.NavigateToResult(firstResult);
}
else
{
// Other keys are not handled.
}

return Task.CompletedTask;
}

private async ValueTask OnDatabaseInitializedAsync()
{
this.SearchIndexCache.Invalidate();
this._searchEntries = Array.Empty<ConfigurationSearchEntry>();
this._searchResults.Clear();
await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(false);
}

private void UpdateSearchResults()
{
this._searchResults.Clear();
if (this._searchEntries.Count == 0)
{
return;
}

var normalizedQuery = Normalize(this._searchText);
if (normalizedQuery.Length < MinimumSearchLength)
{
return;
}

var queryParts = normalizedQuery.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var results = this._searchEntries
.Select(entry => (Entry: entry, Score: CalculateScore(entry, normalizedQuery, queryParts)))
.Where(result => result.Score < int.MaxValue)
.OrderBy(result => result.Score)
.ThenBy(result => result.Entry.Path, StringComparer.Ordinal)
.Take(MaximumResults)
.Select(result => result.Entry);

this._searchResults.AddRange(results);
}

private void NavigateToResult(ConfigurationSearchEntry entry)
{
this._searchText = string.Empty;
this._searchResults.Clear();
this.NavigationHistory.Clear();
this.NavigationManager.NavigateTo(entry.Url);
}
}
Loading