Extract workspace campaign scope coordinator

This commit is contained in:
2026-04-05 00:21:15 +02:00
parent 93c19f0705
commit 25040d7824
3 changed files with 185 additions and 111 deletions

View File

@@ -30,92 +30,15 @@ public partial class Workspace : IAsyncDisposable
return Session.LoadKnownUsernamesAsync();
}
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
{
var campaigns = await WorkspaceQuery.GetCampaignsAsync();
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
private Task ReloadCampaignsAsync(Guid? preferredCampaignId) => Scope.ReloadCampaignsAsync(preferredCampaignId);
if (Campaigns.Count == 0)
{
SelectedCampaignId = null;
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null);
return;
}
private Task ReloadCharacterCampaignOptionsAsync() => Scope.ReloadCharacterCampaignOptionsAsync();
var campaignIds = Campaigns.Select(c => c.Id).ToHashSet();
if (preferredCampaignId.HasValue && campaignIds.Contains(preferredCampaignId.Value))
SelectedCampaignId = preferredCampaignId.Value;
else if (!SelectedCampaignId.HasValue || !campaignIds.Contains(SelectedCampaignId.Value))
SelectedCampaignId = Campaigns[0].Id;
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, SelectedCampaignId?.ToString());
}
private async Task ReloadCharacterCampaignOptionsAsync()
{
var campaignOptions = await WorkspaceQuery.GetCharacterCampaignOptionsAsync();
CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
}
private async Task RefreshCampaignRosterAsync()
{
if (!SelectedCampaignId.HasValue)
{
SelectedCampaign = null;
SelectedCharacterId = null;
return;
}
SelectedCampaign = await WorkspaceQuery.GetCampaignAsync(SelectedCampaignId.Value);
SyncSelectedCharacter();
if (IsPlayScreen && PlaySelectedCharacterId.HasValue && SelectedCharacterId != PlaySelectedCharacterId)
SelectedCharacterId = PlaySelectedCharacterId;
await EnsureSelectedCharacterActiveAsync();
}
private Task RefreshCampaignRosterAsync() => Scope.RefreshCampaignRosterAsync();
private Task RefreshCampaignLogAsync(Guid? afterRollId = null) => Play.RefreshCampaignLogAsync(afterRollId);
private async Task RefreshCampaignScopeAsync()
{
if (!SelectedCampaignId.HasValue)
{
SelectedCampaign = null;
SelectedCharacterSkills = [];
SelectedCharacterSkillGroups = [];
CampaignLog = [];
SelectedCharacterId = null;
ConnectionState = "offline";
CurrentCampaignState = null;
CampaignLogCursor = null;
ResetCampaignLogDetailState();
return;
}
IsCampaignDataLoading = true;
try
{
await RefreshCampaignRosterAsync();
await RefreshSelectedCharacterSheetAsync();
await RefreshCampaignLogAsync();
ResetCampaignStateTracking();
}
catch (ApiRequestException ex) when (ex.StatusCode == 401)
{
ClearAuthenticatedState();
await StopStateEventsAsync();
await LoggedOut.InvokeAsync("Session expired. Please log in again.");
}
catch (ApiRequestException ex)
{
SetStatus(ex.Message, true);
}
finally
{
IsCampaignDataLoading = false;
}
}
private Task RefreshCampaignScopeAsync() => Scope.RefreshCampaignScopeAsync();
private Task LogoutAsync() => Session.LogoutAsync();
@@ -142,11 +65,7 @@ public partial class Workspace : IAsyncDisposable
private Task DeleteUserAsync(AdminUserSummary user) => Admin.DeleteUserAsync(user);
private async Task SetMobilePanelAsync(string panel)
{
MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, MobilePanel);
}
private Task SetMobilePanelAsync(string panel) => Scope.SetMobilePanelAsync(panel);
private Task SetMobilePanelCharacterAsync()
{
@@ -253,27 +172,6 @@ public partial class Workspace : IAsyncDisposable
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
}
private void SyncSelectedCharacter()
{
if (SelectedCampaign is null || SelectedCampaign.Characters.Length == 0)
{
SelectedCharacterId = null;
return;
}
var candidateIds = SelectedCampaign.Characters.Select(c => c.Id).ToHashSet();
if (SelectedCharacterId.HasValue && candidateIds.Contains(SelectedCharacterId.Value))
return;
if (ActiveCharacterId.HasValue && candidateIds.Contains(ActiveCharacterId.Value))
{
SelectedCharacterId = ActiveCharacterId;
return;
}
SelectedCharacterId = SelectedCampaign.Characters[0].Id;
}
private string OwnerLabel(Guid ownerUserId)
{
if (User is not null && ownerUserId == User.Id)
@@ -415,6 +313,19 @@ public partial class Workspace : IAsyncDisposable
private bool IsPlayScreen => State.IsPlayScreen;
private bool IsManagementScreen => State.IsManagementScreen;
private bool IsAdminScreen => State.IsAdminScreen;
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(
State,
Feedback,
JS,
WorkspaceQuery,
EnsureSelectedCharacterActiveAsync,
RefreshSelectedCharacterSheetAsync,
RefreshCampaignLogAsync,
ResetCampaignLogDetailState,
ResetCampaignStateTracking,
ClearAuthenticatedState,
StopStateEventsAsync,
message => LoggedOut.InvokeAsync(message));
private WorkspaceLiveStateController Live => m_Live ??= new(
State,
Feedback,
@@ -489,10 +400,8 @@ public partial class Workspace : IAsyncDisposable
private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString();
private const string ScreenAdmin = "admin";
private const string CampaignSessionKey = "campaign";
private const string MobilePanelSessionKey = "play-panel";
private const int CampaignLogWindowSize = 25;
private WorkspaceCampaignScopeCoordinator? m_Scope;
private WorkspaceLiveStateController? m_Live;
private WorkspacePlayCoordinator? m_Play;
private WorkspaceCampaignCoordinator? m_CampaignsFlow;

View File

@@ -0,0 +1,165 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;
using RpgRoller.Contracts;
namespace RpgRoller.Components.Pages;
[ExcludeFromCodeCoverage]
public sealed class WorkspaceCampaignScopeCoordinator
{
public WorkspaceCampaignScopeCoordinator(
WorkspaceState state,
WorkspaceFeedbackService feedback,
IJSRuntime js,
WorkspaceQueryService workspaceQuery,
Func<Task> ensureSelectedCharacterActiveAsync,
Func<Task> refreshSelectedCharacterSheetAsync,
Func<Guid?, Task> refreshCampaignLogAsync,
Action resetCampaignLogDetailState,
Action resetCampaignStateTracking,
Action clearAuthenticatedState,
Func<Task> stopStateEventsAsync,
Func<string?, Task> onLoggedOutAsync)
{
m_State = state;
m_Feedback = feedback;
m_JS = js;
m_WorkspaceQuery = workspaceQuery;
m_EnsureSelectedCharacterActiveAsync = ensureSelectedCharacterActiveAsync;
m_RefreshSelectedCharacterSheetAsync = refreshSelectedCharacterSheetAsync;
m_RefreshCampaignLogAsync = refreshCampaignLogAsync;
m_ResetCampaignLogDetailState = resetCampaignLogDetailState;
m_ResetCampaignStateTracking = resetCampaignStateTracking;
m_ClearAuthenticatedState = clearAuthenticatedState;
m_StopStateEventsAsync = stopStateEventsAsync;
m_OnLoggedOutAsync = onLoggedOutAsync;
}
public async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
{
var campaigns = await m_WorkspaceQuery.GetCampaignsAsync();
m_State.Campaigns = campaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
if (m_State.Campaigns.Count == 0)
{
m_State.SelectedCampaignId = null;
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null);
return;
}
var campaignIds = m_State.Campaigns.Select(campaign => campaign.Id).ToHashSet();
if (preferredCampaignId.HasValue && campaignIds.Contains(preferredCampaignId.Value))
m_State.SelectedCampaignId = preferredCampaignId.Value;
else if (!m_State.SelectedCampaignId.HasValue || !campaignIds.Contains(m_State.SelectedCampaignId.Value))
m_State.SelectedCampaignId = m_State.Campaigns[0].Id;
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, m_State.SelectedCampaignId?.ToString());
}
public async Task ReloadCharacterCampaignOptionsAsync()
{
var campaignOptions = await m_WorkspaceQuery.GetCharacterCampaignOptionsAsync();
m_State.CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
}
public async Task RefreshCampaignRosterAsync()
{
if (!m_State.SelectedCampaignId.HasValue)
{
m_State.SelectedCampaign = null;
m_State.SelectedCharacterId = null;
return;
}
m_State.SelectedCampaign = await m_WorkspaceQuery.GetCampaignAsync(m_State.SelectedCampaignId.Value);
SyncSelectedCharacter();
if (m_State.IsPlayScreen && m_State.PlaySelectedCharacterId.HasValue && m_State.SelectedCharacterId != m_State.PlaySelectedCharacterId)
m_State.SelectedCharacterId = m_State.PlaySelectedCharacterId;
await m_EnsureSelectedCharacterActiveAsync();
}
public async Task RefreshCampaignScopeAsync()
{
if (!m_State.SelectedCampaignId.HasValue)
{
m_State.SelectedCampaign = null;
m_State.SelectedCharacterSkills = [];
m_State.SelectedCharacterSkillGroups = [];
m_State.CampaignLog = [];
m_State.SelectedCharacterId = null;
m_State.ConnectionState = "offline";
m_State.CurrentCampaignState = null;
m_State.CampaignLogCursor = null;
m_ResetCampaignLogDetailState();
return;
}
m_State.IsCampaignDataLoading = true;
try
{
await RefreshCampaignRosterAsync();
await m_RefreshSelectedCharacterSheetAsync();
await m_RefreshCampaignLogAsync(null);
m_ResetCampaignStateTracking();
}
catch (ApiRequestException ex) when (ex.StatusCode == 401)
{
m_ClearAuthenticatedState();
await m_StopStateEventsAsync();
await m_OnLoggedOutAsync("Session expired. Please log in again.");
}
catch (ApiRequestException ex)
{
m_Feedback.SetStatus(ex.Message, true);
}
finally
{
m_State.IsCampaignDataLoading = false;
}
}
public async Task SetMobilePanelAsync(string panel)
{
m_State.MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, m_State.MobilePanel);
}
private void SyncSelectedCharacter()
{
if (m_State.SelectedCampaign is null || m_State.SelectedCampaign.Characters.Length == 0)
{
m_State.SelectedCharacterId = null;
return;
}
var candidateIds = m_State.SelectedCampaign.Characters.Select(character => character.Id).ToHashSet();
if (m_State.SelectedCharacterId.HasValue && candidateIds.Contains(m_State.SelectedCharacterId.Value))
return;
if (m_State.ActiveCharacterId.HasValue && candidateIds.Contains(m_State.ActiveCharacterId.Value))
{
m_State.SelectedCharacterId = m_State.ActiveCharacterId;
return;
}
m_State.SelectedCharacterId = m_State.SelectedCampaign.Characters[0].Id;
}
private readonly Action m_ClearAuthenticatedState;
private readonly Func<Task> m_EnsureSelectedCharacterActiveAsync;
private readonly WorkspaceFeedbackService m_Feedback;
private readonly IJSRuntime m_JS;
private readonly Func<string?, Task> m_OnLoggedOutAsync;
private readonly Func<Guid?, Task> m_RefreshCampaignLogAsync;
private readonly Func<Task> m_RefreshSelectedCharacterSheetAsync;
private readonly Action m_ResetCampaignLogDetailState;
private readonly Action m_ResetCampaignStateTracking;
private readonly WorkspaceState m_State;
private readonly Func<Task> m_StopStateEventsAsync;
private readonly WorkspaceQueryService m_WorkspaceQuery;
private const string CampaignSessionKey = "campaign";
private const string MobilePanelSessionKey = "play-panel";
}