replace Bazored.Toast with custom Toast component
This commit is contained in:
@@ -1,12 +1,6 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<!--
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
<BlazoredToasts />
|
||||
-->
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<BreadcrumbNavigation />
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@using Blazored.Toast
|
||||
@using Blazored.Toast.Services
|
||||
|
||||
@using BlazorInputFile
|
||||
|
||||
@using MUnique.OpenMU.Web.Shared
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<PackageVersion Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageVersion Include="BlazorInputFile" Version="0.2.0" />
|
||||
<PackageVersion Include="Blazored.Toast" Version="4.2.1" />
|
||||
<PackageVersion Include="BuildWebCompiler2022" Version="1.14.15" />
|
||||
<PackageVersion Include="DG.AdvancedDataGridView" Version="1.2.30115.18" />
|
||||
<PackageVersion Include="Dapr.AspNetCore" Version="1.16.1" />
|
||||
|
||||
@@ -92,69 +92,6 @@ internal class EntityFrameworkContextBase : IContext
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is a transient conflict caused by a concurrent entity mutation
|
||||
/// racing this save, and is therefore worth retrying.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception thrown by the save.</param>
|
||||
/// <returns><c>true</c> if the save should be retried.</returns>
|
||||
private static bool IsTransientConcurrencyConflict(Exception exception)
|
||||
{
|
||||
// A concurrent entity mutation racing this save corrupts the change tracker mid-enumeration.
|
||||
// Depending on exactly where change detection was, it surfaces as one of several types - a
|
||||
// modified collection (InvalidOperationException), a transiently-null internal key
|
||||
// (ArgumentNullException/NullReferenceException), or an out-of-range index. All are transient:
|
||||
// the racing mutation is a single quick operation, so a bounded retry lands on a stable moment.
|
||||
// A genuinely persistent error of the same type is not masked - it rethrows once the retries
|
||||
// are exhausted. The deterministic serialization (per-player persistence lock) is the primary
|
||||
// guard; this retry only needs to absorb the rare, bursty sources that lock isn't held for.
|
||||
return exception is DbUpdateConcurrencyException
|
||||
or InvalidOperationException
|
||||
or ArgumentNullException
|
||||
or NullReferenceException
|
||||
or IndexOutOfRangeException
|
||||
or KeyNotFoundException;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> SaveChangesCoreAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var l = await this._lock.LockAsync();
|
||||
|
||||
// when we have a change publisher attached, we want to get the changed entries before accepting them.
|
||||
// Otherwise, we can accept them.
|
||||
var acceptChanges = true;
|
||||
|
||||
object? sender = null;
|
||||
SavedChangesEventArgs? args = null;
|
||||
if (this._changeListener is { })
|
||||
{
|
||||
this.Context.SavedChanges += OnSavedChanges;
|
||||
acceptChanges = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await this.Context.SaveChangesAsync(acceptChanges, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (args is not null)
|
||||
{
|
||||
await this.OnSavedChangesAsync(sender, args).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Context.SavedChanges -= OnSavedChanges;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
void OnSavedChanges(object? s, SavedChangesEventArgs e)
|
||||
{
|
||||
sender = s;
|
||||
args = e;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable SuspendChangeNotifications()
|
||||
{
|
||||
@@ -323,6 +260,69 @@ internal class EntityFrameworkContextBase : IContext
|
||||
this.Context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the exception is a transient conflict caused by a concurrent entity mutation
|
||||
/// racing this save, and is therefore worth retrying.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception thrown by the save.</param>
|
||||
/// <returns><c>true</c> if the save should be retried.</returns>
|
||||
private static bool IsTransientConcurrencyConflict(Exception exception)
|
||||
{
|
||||
// A concurrent entity mutation racing this save corrupts the change tracker mid-enumeration.
|
||||
// Depending on exactly where change detection was, it surfaces as one of several types - a
|
||||
// modified collection (InvalidOperationException), a transiently-null internal key
|
||||
// (ArgumentNullException/NullReferenceException), or an out-of-range index. All are transient:
|
||||
// the racing mutation is a single quick operation, so a bounded retry lands on a stable moment.
|
||||
// A genuinely persistent error of the same type is not masked - it rethrows once the retries
|
||||
// are exhausted. The deterministic serialization (per-player persistence lock) is the primary
|
||||
// guard; this retry only needs to absorb the rare, bursty sources that lock isn't held for.
|
||||
return exception is DbUpdateConcurrencyException
|
||||
or InvalidOperationException
|
||||
or ArgumentNullException
|
||||
or NullReferenceException
|
||||
or IndexOutOfRangeException
|
||||
or KeyNotFoundException;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> SaveChangesCoreAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var l = await this._lock.LockAsync();
|
||||
|
||||
// when we have a change publisher attached, we want to get the changed entries before accepting them.
|
||||
// Otherwise, we can accept them.
|
||||
var acceptChanges = true;
|
||||
|
||||
object? sender = null;
|
||||
SavedChangesEventArgs? args = null;
|
||||
if (this._changeListener is { })
|
||||
{
|
||||
this.Context.SavedChanges += OnSavedChanges;
|
||||
acceptChanges = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await this.Context.SaveChangesAsync(acceptChanges, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (args is not null)
|
||||
{
|
||||
await this.OnSavedChangesAsync(sender, args).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Context.SavedChanges -= OnSavedChanges;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
void OnSavedChanges(object? s, SavedChangesEventArgs e)
|
||||
{
|
||||
sender = s;
|
||||
args = e;
|
||||
}
|
||||
}
|
||||
|
||||
private bool DetachInternal(object item)
|
||||
{
|
||||
var entry = this.Context.Entry(item);
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
TaxStore = table.Column<byte>(type: "smallint", nullable: false),
|
||||
TaxHunt = table.Column<int>(type: "integer", nullable: false),
|
||||
IsHuntZoneEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TributeMoney = table.Column<long>(type: "bigint", nullable: false)
|
||||
TributeMoney = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -53,7 +53,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
DefenseLevel = table.Column<byte>(type: "smallint", nullable: false),
|
||||
RegenLevel = table.Column<byte>(type: "smallint", nullable: false),
|
||||
LifeLevel = table.Column<byte>(type: "smallint", nullable: false),
|
||||
CurrentHp = table.Column<int>(type: "integer", nullable: false)
|
||||
CurrentHp = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -87,7 +87,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
GuildScoreCastleSiege = table.Column<int>(type: "integer", nullable: false),
|
||||
GuildScoreCastleSiegeMembers = table.Column<int>(type: "integer", nullable: false),
|
||||
GateBuyPrice = table.Column<int>(type: "integer", nullable: false),
|
||||
StatueBuyPrice = table.Column<int>(type: "integer", nullable: false)
|
||||
StatueBuyPrice = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -125,7 +125,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
DefaultSide = table.Column<byte>(type: "smallint", nullable: false),
|
||||
SpawnX = table.Column<byte>(type: "smallint", nullable: false),
|
||||
SpawnY = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Direction = table.Column<int>(type: "integer", nullable: false)
|
||||
Direction = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -155,7 +155,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
State = table.Column<byte>(type: "smallint", nullable: false),
|
||||
DayOfWeek = table.Column<int>(type: "integer", nullable: false),
|
||||
Hour = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Minute = table.Column<byte>(type: "smallint", nullable: false)
|
||||
Minute = table.Column<byte>(type: "smallint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -183,7 +183,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
Level = table.Column<byte>(type: "smallint", nullable: false),
|
||||
RequiredJewelOfGuardianCount = table.Column<int>(type: "integer", nullable: false),
|
||||
RequiredZen = table.Column<int>(type: "integer", nullable: false),
|
||||
Value = table.Column<int>(type: "integer", nullable: false)
|
||||
Value = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -236,7 +236,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
|
||||
X1 = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Y1 = table.Column<byte>(type: "smallint", nullable: false),
|
||||
X2 = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Y2 = table.Column<byte>(type: "smallint", nullable: false)
|
||||
Y2 = table.Column<byte>(type: "smallint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
@implements IDisposable
|
||||
@inject CreationPanelService Panel
|
||||
@inject Blazored.Toast.Services.IToastService ToastService
|
||||
@inject IToastService ToastService
|
||||
|
||||
@if (this.Panel.Current is { } session)
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BlazoredToasts />
|
||||
<ToastContainer />
|
||||
|
||||
<article class="content px-4 py-3">
|
||||
@Body
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" />
|
||||
<PackageReference Include="Blazored.Toast" />
|
||||
<PackageReference Include="BlazorInputFile" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" />
|
||||
<PackageReference Include="Nito.AsyncEx" />
|
||||
|
||||
@@ -6,12 +6,12 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
using Blazored.Toast.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Properties;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
using MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,13 +6,13 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
using Blazored.Toast.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Properties;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Modal;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
using MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Blazored.Toast.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
@@ -19,6 +18,7 @@ using MUnique.OpenMU.Web.AdminPanel.Properties;
|
||||
using MUnique.OpenMU.Web.Shared;
|
||||
using MUnique.OpenMU.Web.Shared.Components;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Modal;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
using MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Blazored.Toast.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.QuickGrid;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -18,6 +17,7 @@ using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Properties;
|
||||
using MUnique.OpenMU.Web.Shared;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Modal;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
using MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Blazored.Toast.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
@@ -19,6 +18,7 @@ using MUnique.OpenMU.Web.Shared;
|
||||
using MUnique.OpenMU.Web.Shared.Components;
|
||||
using MUnique.OpenMU.Web.Shared.Components.MapEditor;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Modal;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
using MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using Blazored.Toast.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.QuickGrid;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
@@ -16,6 +15,7 @@ using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.DataModel.Entities;
|
||||
using MUnique.OpenMU.Persistence;
|
||||
using MUnique.OpenMU.Web.AdminPanel.Properties;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
using MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
@using System.ComponentModel
|
||||
@using Microsoft.Extensions.DependencyInjection
|
||||
@using Blazored.Toast.Services
|
||||
@using MUnique.OpenMU.Interfaces
|
||||
@using MUnique.OpenMU.Web.AdminPanel.Properties
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
namespace MUnique.OpenMU.Web.AdminPanel;
|
||||
|
||||
using System.IO;
|
||||
using Blazored.Toast;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -61,7 +60,7 @@ public class Startup
|
||||
.ConfigureApplicationPartManager(setup =>
|
||||
setup.FeatureProviders.Add(new GenericControllerFeatureProvider()));
|
||||
|
||||
services.AddBlazoredToast();
|
||||
services.AddToasts();
|
||||
services.AddScoped<ModalService>();
|
||||
services.AddScoped<IModalService>(sp => sp.GetRequiredService<ModalService>());
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
namespace MUnique.OpenMU.Web.AdminPanel;
|
||||
|
||||
using System.IO;
|
||||
using Blazored.Toast;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -66,7 +65,7 @@ public static class WebApplicationExtensions
|
||||
.ConfigureApplicationPartManager(setup =>
|
||||
setup.FeatureProviders.Add(new GenericControllerFeatureProvider()));
|
||||
|
||||
services.AddBlazoredToast();
|
||||
services.AddToasts();
|
||||
|
||||
services.AddScoped<ModalService>();
|
||||
services.AddScoped<IModalService>(sp => sp.GetRequiredService<ModalService>());
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@using Blazored.Toast
|
||||
@using Blazored.Toast.Services
|
||||
|
||||
@using BlazorInputFile
|
||||
|
||||
@using MUnique.OpenMU.Web.AdminPanel
|
||||
@@ -20,6 +17,7 @@
|
||||
|
||||
@using MUnique.OpenMU.Web.Shared
|
||||
@using MUnique.OpenMU.Web.Shared.Components
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Toast
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Modal
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Form
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Form.Modal
|
||||
|
||||
63
src/Web/Shared/Components/Toast/IToastService.cs
Normal file
63
src/Web/Shared/Components/Toast/IToastService.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
// <copyright file="IToastService.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Service for showing toast notifications.
|
||||
/// </summary>
|
||||
public interface IToastService
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the list of toasts has changed (added, closed, cleared).
|
||||
/// </summary>
|
||||
event Action? StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently shown toasts.
|
||||
/// </summary>
|
||||
IReadOnlyList<ToastInstance> Toasts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows a success toast.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="heading">The optional heading.</param>
|
||||
void ShowSuccess(string message, string? heading = null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an info toast.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="heading">The optional heading.</param>
|
||||
void ShowInfo(string message, string? heading = null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a warning toast.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="heading">The optional heading.</param>
|
||||
void ShowWarning(string message, string? heading = null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an error toast.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="heading">The optional heading.</param>
|
||||
void ShowError(string message, string? heading = null);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the specified toast (triggers its closing animation).
|
||||
/// </summary>
|
||||
/// <param name="toast">The toast to close.</param>
|
||||
void Close(ToastInstance toast);
|
||||
|
||||
/// <summary>
|
||||
/// Closes all currently shown toasts.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
}
|
||||
66
src/Web/Shared/Components/Toast/ToastContainer.razor
Normal file
66
src/Web/Shared/Components/Toast/ToastContainer.razor
Normal file
@@ -0,0 +1,66 @@
|
||||
@using MUnique.OpenMU.Web.Shared.Components.Toast
|
||||
@using MUnique.OpenMU.Web.Shared.Services
|
||||
@inject ToastService ToastService
|
||||
@implements IDisposable
|
||||
|
||||
@if (this.ToastService.Toasts.Count > 0)
|
||||
{
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" aria-live="polite" aria-atomic="true">
|
||||
@foreach (var toast in this.ToastService.Toasts)
|
||||
{
|
||||
var (iconClass, accentClass) = this.GetStyling(toast.Level);
|
||||
<div @key="toast.Key" class="toast @accentClass @(toast.IsClosing ? "closing" : "show")" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body d-flex align-items-start">
|
||||
<span class="oi @iconClass toast__icon" aria-hidden="true"></span>
|
||||
<div class="ms-2 w-100">
|
||||
@if (toast.Heading is { } heading)
|
||||
{
|
||||
<strong class="d-block">@heading</strong>
|
||||
}
|
||||
<div>@toast.Message</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" @onclick="() => this.CloseAsync(toast)"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
this.ToastService.StateChanged += this.OnStateChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.ToastService.StateChanged -= this.OnStateChanged;
|
||||
}
|
||||
|
||||
private void OnStateChanged()
|
||||
{
|
||||
_ = this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private Task CloseAsync(ToastInstance toast)
|
||||
{
|
||||
this.ToastService.Close(toast);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private (string IconClass, string AccentClass) GetStyling(ToastLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
ToastLevel.Success => ("oi-circle-check", "text-success"),
|
||||
ToastLevel.Info => ("oi-info", "text-primary"),
|
||||
ToastLevel.Warning => ("oi-warning", "text-warning"),
|
||||
ToastLevel.Error => ("oi-bolt", "text-danger"),
|
||||
_ => ("oi-info", "text-primary"),
|
||||
};
|
||||
}
|
||||
}
|
||||
34
src/Web/Shared/Components/Toast/ToastContainer.razor.css
Normal file
34
src/Web/Shared/Components/Toast/ToastContainer.razor.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.toast-container {
|
||||
z-index: 1080;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: 18rem;
|
||||
border-left: 4px solid currentColor;
|
||||
background-color: var(--bs-body-bg);
|
||||
box-shadow: var(--bs-box-shadow-lg);
|
||||
animation: toast-slide-in 0.2s ease-out;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.closing {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toast__icon {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@keyframes toast-slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
52
src/Web/Shared/Components/Toast/ToastInstance.cs
Normal file
52
src/Web/Shared/Components/Toast/ToastInstance.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// <copyright file="ToastInstance.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single toast message shown in the <see cref="ToastContainer"/>.
|
||||
/// </summary>
|
||||
public sealed class ToastInstance
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ToastInstance"/> class.
|
||||
/// </summary>
|
||||
/// <param name="level">The level.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="heading">The optional heading.</param>
|
||||
internal ToastInstance(ToastLevel level, string message, string? heading)
|
||||
{
|
||||
this.Key = Guid.NewGuid();
|
||||
this.Level = level;
|
||||
this.Message = message;
|
||||
this.Heading = heading;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a stable key identifying this toast, used as a render key.
|
||||
/// </summary>
|
||||
public Guid Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level.
|
||||
/// </summary>
|
||||
public ToastLevel Level { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional heading.
|
||||
/// </summary>
|
||||
public string? Heading { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the toast is performing its closing animation.
|
||||
/// </summary>
|
||||
internal bool IsClosing { get; set; }
|
||||
}
|
||||
31
src/Web/Shared/Components/Toast/ToastLevel.cs
Normal file
31
src/Web/Shared/Components/Toast/ToastLevel.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// <copyright file="ToastLevel.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
|
||||
/// <summary>
|
||||
/// The level of a toast message.
|
||||
/// </summary>
|
||||
public enum ToastLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational message.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Success message.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// Warning message.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Error message.
|
||||
/// </summary>
|
||||
Error,
|
||||
}
|
||||
@@ -52,7 +52,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" />
|
||||
<PackageReference Include="Blazored.Toast" />
|
||||
<PackageReference Include="BlazorInputFile" />
|
||||
<PackageReference Include="Mapster" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" />
|
||||
|
||||
172
src/Web/Shared/Services/ToastService.cs
Normal file
172
src/Web/Shared/Services/ToastService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
// <copyright file="ToastService.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IToastService"/>.
|
||||
/// </summary>
|
||||
public sealed class ToastService : IToastService, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan ClosingDuration = TimeSpan.FromMilliseconds(300);
|
||||
|
||||
private readonly object _lock = new();
|
||||
private readonly List<ToastInstance> _toasts = new();
|
||||
private readonly List<CancellationTokenSource> _cancellations = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? StateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ToastInstance> Toasts
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
return this._toasts.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ShowSuccess(string message, string? heading = null)
|
||||
{
|
||||
this.Show(ToastLevel.Success, message, heading);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ShowInfo(string message, string? heading = null)
|
||||
{
|
||||
this.Show(ToastLevel.Info, message, heading);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ShowWarning(string message, string? heading = null)
|
||||
{
|
||||
this.Show(ToastLevel.Warning, message, heading);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ShowError(string message, string? heading = null)
|
||||
{
|
||||
this.Show(ToastLevel.Error, message, heading);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Close(ToastInstance toast)
|
||||
{
|
||||
this.StartClosing(toast);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Clear()
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
foreach (var cts in this._cancellations)
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
this._cancellations.Clear();
|
||||
this._toasts.Clear();
|
||||
}
|
||||
|
||||
this.StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
foreach (var cts in this._cancellations)
|
||||
{
|
||||
cts.Dispose();
|
||||
}
|
||||
|
||||
this._cancellations.Clear();
|
||||
this._toasts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void Show(ToastLevel level, string message, string? heading)
|
||||
{
|
||||
var toast = new ToastInstance(level, message, heading);
|
||||
var cts = new CancellationTokenSource();
|
||||
lock (this._lock)
|
||||
{
|
||||
this._toasts.Add(toast);
|
||||
this._cancellations.Add(cts);
|
||||
}
|
||||
|
||||
this.StateChanged?.Invoke();
|
||||
|
||||
_ = this.AutoCloseAsync(toast, cts);
|
||||
}
|
||||
|
||||
private async Task AutoCloseAsync(ToastInstance toast, CancellationTokenSource cts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(DefaultDuration, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.StartClosing(toast);
|
||||
}
|
||||
|
||||
private void StartClosing(ToastInstance toast)
|
||||
{
|
||||
lock (this._lock)
|
||||
{
|
||||
var index = this._toasts.IndexOf(toast);
|
||||
if (index < 0 || toast.IsClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
toast.IsClosing = true;
|
||||
this._cancellations[index].Cancel();
|
||||
}
|
||||
|
||||
this.StateChanged?.Invoke();
|
||||
|
||||
_ = this.FinishClosingAsync(toast);
|
||||
}
|
||||
|
||||
private Task FinishClosingAsync(ToastInstance toast)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(ClosingDuration).ConfigureAwait(false);
|
||||
|
||||
lock (this._lock)
|
||||
{
|
||||
var index = this._toasts.IndexOf(toast);
|
||||
if (index < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancellations[index].Dispose();
|
||||
this._cancellations.RemoveAt(index);
|
||||
this._toasts.RemoveAt(index);
|
||||
}
|
||||
|
||||
this.StateChanged?.Invoke();
|
||||
});
|
||||
}
|
||||
}
|
||||
26
src/Web/Shared/Services/ToastServiceCollectionExtensions.cs
Normal file
26
src/Web/Shared/Services/ToastServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// <copyright file="ToastServiceCollectionExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Web.Shared.Services;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MUnique.OpenMU.Web.Shared.Components.Toast;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering the toast service.
|
||||
/// </summary>
|
||||
public static class ToastServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the toast service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection, for chaining.</returns>
|
||||
public static IServiceCollection AddToasts(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ToastService>();
|
||||
services.AddScoped<IToastService>(sp => sp.GetRequiredService<ToastService>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@using Blazored.Toast
|
||||
@using Blazored.Toast.Services
|
||||
|
||||
@using BlazorInputFile
|
||||
|
||||
@using MUnique.OpenMU.Web.Shared
|
||||
|
||||
Reference in New Issue
Block a user