From 0124325c20be0083eb1efca92dd88ad9731057c2 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sat, 4 Apr 2026 23:57:59 +0200 Subject: [PATCH] Extract workspace feedback service --- README.md | 2 +- RpgRoller/Components/Pages/Workspace.razor.cs | 34 +++--------- .../Pages/WorkspaceFeedbackService.cs | 54 +++++++++++++++++++ 3 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 RpgRoller/Components/Pages/WorkspaceFeedbackService.cs diff --git a/README.md b/README.md index 04f0af7..57d55c4 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Frontend: - `RpgRoller/Components/Pages/Home.razor`: minimal gateway page (loading/auth/workspace switch) - `RpgRoller/Components/Pages/Home.razor.cs`: single `Home` code-behind with only gateway/session-view orchestration - `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI and workspace-specific state/logic -- `RpgRoller/Components/Pages/WorkspaceState.cs` and `WorkspaceToast.cs`: extracted workspace UI state, toast records, and pure computed projections while `Workspace` remains the behavior owner +- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, and `WorkspaceFeedbackService.cs`: extracted workspace UI state, toast records, and feedback/status handling while `Workspace` remains the behavior owner - `RpgRoller/Components/**/*.razor.cs`: component code-behind classes (state, handlers, parameters, injected dependencies); `.razor` files remain markup-focused - `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState` + page form models - `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor` diff --git a/RpgRoller/Components/Pages/Workspace.razor.cs b/RpgRoller/Components/Pages/Workspace.razor.cs index 2c53753..df9b478 100644 --- a/RpgRoller/Components/Pages/Workspace.razor.cs +++ b/RpgRoller/Components/Pages/Workspace.razor.cs @@ -1077,41 +1077,17 @@ public partial class Workspace : IAsyncDisposable AdminUsers = []; HasLoadedAdminUsers = false; IsAdminDataLoading = false; - Toasts.Clear(); + Feedback.ClearToasts(); } private void SetStatus(string message, bool isError) { - AddToast(message, isError); - Announce(message); + Feedback.SetStatus(message, isError); } private void Announce(string message) { - LiveAnnouncement = message; - } - - private void AddToast(string message, bool isError) - { - var toastId = Guid.NewGuid(); - Toasts.Add(new WorkspaceToast(toastId, message, isError)); - _ = DismissToastLaterAsync(toastId); - } - - private async Task DismissToastLaterAsync(Guid toastId) - { - await Task.Delay(ToastDurationMs); - - if (Toasts.RemoveAll(toast => toast.Id == toastId) == 0) - return; - - try - { - await InvokeAsync(StateHasChanged); - } - catch (ObjectDisposedException) - { - } + Feedback.Announce(message); } private void ToggleScreenMenu() @@ -1220,6 +1196,7 @@ public partial class Workspace : IAsyncDisposable private bool IsPlayScreen => State.IsPlayScreen; private bool IsManagementScreen => State.IsManagementScreen; private bool IsAdminScreen => State.IsAdminScreen; + private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged)); private IReadOnlyList HeaderMenuItems { get @@ -1250,5 +1227,6 @@ public partial class Workspace : IAsyncDisposable private const string MobilePanelSessionKey = "play-panel"; private const string RollVisibilitySessionKey = "roll-visibility"; private const int CampaignLogWindowSize = 25; - private const int ToastDurationMs = 3200; + + private WorkspaceFeedbackService? m_Feedback; } diff --git a/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs b/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs new file mode 100644 index 0000000..c660c6a --- /dev/null +++ b/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs @@ -0,0 +1,54 @@ +namespace RpgRoller.Components.Pages; + +public sealed class WorkspaceFeedbackService +{ + public WorkspaceFeedbackService(WorkspaceState state, Func requestRefreshAsync) + { + m_State = state; + m_RequestRefreshAsync = requestRefreshAsync; + } + + public void SetStatus(string message, bool isError) + { + Announce(message); + AddToast(message, isError); + } + + public void Announce(string message) + { + m_State.LiveAnnouncement = message; + } + + public void ClearToasts() + { + m_State.Toasts.Clear(); + } + + private void AddToast(string message, bool isError) + { + var toastId = Guid.NewGuid(); + m_State.Toasts.Add(new WorkspaceToast(toastId, message, isError)); + _ = DismissToastLaterAsync(toastId); + } + + private async Task DismissToastLaterAsync(Guid toastId) + { + await Task.Delay(ToastDurationMs); + + if (m_State.Toasts.RemoveAll(toast => toast.Id == toastId) == 0) + return; + + try + { + await m_RequestRefreshAsync(); + } + catch (ObjectDisposedException) + { + } + } + + private readonly Func m_RequestRefreshAsync; + private readonly WorkspaceState m_State; + + private const int ToastDurationMs = 3200; +}