Skip to content
Open
Show file tree
Hide file tree
Changes from all 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