Extract workspace play coordinator

This commit is contained in:
2026-04-05 00:16:16 +02:00
parent ec40baa107
commit 4d5d112168
3 changed files with 382 additions and 294 deletions

View File

@@ -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`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, and `WorkspaceCampaignCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, and campaign-management/modal flows while `Workspace` remains the behavior owner
- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, `WorkspaceCampaignCoordinator.cs`, and `WorkspacePlayCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, campaign-management/modal flows, and play/log coordination 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<TModel>` + page form models
- `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor`

View File

@@ -75,57 +75,7 @@ public partial class Workspace : IAsyncDisposable
await EnsureSelectedCharacterActiveAsync();
}
private async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
{
if (!SelectedCampaignId.HasValue || !IsPlayScreen)
{
CampaignLog = [];
CampaignLogCursor = null;
ResetCampaignLogDetailState();
return;
}
var previousLogCount = CampaignLog.Count;
var page = await WorkspaceQuery.GetCampaignLogPageAsync(SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
Guid? newestRollId = null;
if (!afterRollId.HasValue || page.ResetRequired)
{
CampaignLog = page.Entries.ToList();
}
else if (page.Entries.Length > 0)
{
CampaignLog.AddRange(page.Entries);
if (CampaignLog.Count > CampaignLogWindowSize)
CampaignLog = CampaignLog.TakeLast(CampaignLogWindowSize).ToList();
}
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
if (!shouldAutoExpandNewest &&
!afterRollId.HasValue &&
CurrentCampaignState is not null &&
previousLogCount == 0 &&
page.Entries.Length > 0)
{
shouldAutoExpandNewest = true;
}
if (shouldAutoExpandNewest)
{
newestRollId = page.Entries[^1].RollId;
ExpandedCampaignLogRollId = newestRollId;
FreshCampaignLogRollId = newestRollId;
}
else if (!afterRollId.HasValue)
{
FreshCampaignLogRollId = null;
}
CampaignLogCursor = page.Cursor ?? afterRollId;
TrimCampaignLogDetails();
if (newestRollId.HasValue)
await EnsureRollDetailLoadedAsync(newestRollId.Value);
}
private Task RefreshCampaignLogAsync(Guid? afterRollId = null) => Play.RefreshCampaignLogAsync(afterRollId);
private async Task RefreshCampaignScopeAsync()
{
@@ -226,188 +176,41 @@ public partial class Workspace : IAsyncDisposable
private Task DeleteCharacterAsync(CharacterSummary character) => CampaignsFlow.DeleteCharacterAsync(character);
private async Task SelectCharacterAsync(Guid characterId)
{
SelectedCharacterId = characterId;
await RefreshSelectedCharacterSheetAsync();
await EnsureSelectedCharacterActiveAsync();
}
private Task SelectCharacterAsync(Guid characterId) => Play.SelectCharacterAsync(characterId);
private bool CanEditCharacter(CharacterSummary character) => CampaignsFlow.CanEditCharacter(character);
private bool CanDeleteCharacter(CharacterSummary character) => CampaignsFlow.CanDeleteCharacter(character);
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
{
return user is not null && character.OwnerUserId == user.Id;
}
private Task EnsureSelectedCharacterActiveAsync() => Play.EnsureSelectedCharacterActiveAsync();
private async Task EnsureSelectedCharacterActiveAsync()
{
if (!SelectedCharacterId.HasValue || SelectedCampaign is null)
return;
private Task RefreshSelectedCharacterSheetAsync() => Play.RefreshSelectedCharacterSheetAsync();
var character = SelectedCampaign.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId.Value);
if (character is null || !CanActivateCharacter(character, User) || ActiveCharacterId == character.Id)
return;
private Task ToggleRollDetailAsync(Guid rollId) => Play.ToggleRollDetailAsync(rollId);
try
{
await ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
ActiveCharacterId = character.Id;
}
catch (ApiRequestException ex)
{
SetStatus(ex.Message, true);
}
}
private Task OnSkillCreatedAsync(Guid id) => Play.OnSkillCreatedAsync(id);
private async Task RefreshSelectedCharacterSheetAsync()
{
if (!SelectedCharacterId.HasValue || SelectedCampaign is null || !IsPlayScreen)
{
SelectedCharacterSkills = [];
SelectedCharacterSkillGroups = [];
return;
}
private Task OnSkillUpdatedAsync(Guid id) => Play.OnSkillUpdatedAsync(id);
var sheet = await WorkspaceQuery.GetCharacterSheetAsync(SelectedCharacterId.Value);
SelectedCharacterSkillGroups = sheet.SkillGroups
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
SelectedCharacterSkills = sheet.Skills
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private Task OnSkillGroupCreatedAsync(Guid id) => Play.OnSkillGroupCreatedAsync(id);
private async Task ToggleRollDetailAsync(Guid rollId)
{
if (ExpandedCampaignLogRollId == rollId)
{
ExpandedCampaignLogRollId = null;
if (FreshCampaignLogRollId == rollId)
FreshCampaignLogRollId = null;
return;
}
private Task OnSkillGroupUpdatedAsync(Guid id) => Play.OnSkillGroupUpdatedAsync(id);
ExpandedCampaignLogRollId = rollId;
FreshCampaignLogRollId = null;
await EnsureRollDetailLoadedAsync(rollId);
}
private Task OnSkillDeletedAsync(Guid id) => Play.OnSkillDeletedAsync(id);
private async Task OnSkillCreatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill created.", false);
}
private Task OnSkillGroupDeletedAsync(Guid id) => Play.OnSkillGroupDeletedAsync(id);
private async Task OnSkillUpdatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill updated.", false);
}
private Task OnCharacterPanelErrorAsync(string message) => Play.OnCharacterPanelErrorAsync(message);
private async Task OnSkillGroupCreatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill group created.", false);
}
private Task OnCampaignLogPanelErrorAsync(string message) => Play.OnCampaignLogPanelErrorAsync(message);
private async Task OnSkillGroupUpdatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill group updated.", false);
}
private Task RollSkillAsync(Guid skillId) => Play.RollSkillAsync(skillId);
private async Task OnSkillDeletedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill deleted.", false);
}
private async Task OnSkillGroupDeletedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill group deleted.", false);
}
private Task OnCharacterPanelErrorAsync(string message)
{
SetStatus(message, true);
return Task.CompletedTask;
}
private Task OnCampaignLogPanelErrorAsync(string message)
{
SetStatus(message, true);
return Task.CompletedTask;
}
private async Task RollSkillAsync(Guid skillId)
{
if (SelectedCampaign is null)
{
SetStatus("No campaign selected.", true);
return;
}
IsMutating = true;
try
{
var roll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
await HandleRecordedRollAsync(roll);
}
catch (ApiRequestException ex)
{
SetStatus(ex.Message, true);
}
finally
{
IsMutating = false;
}
}
private async Task OnCustomRollCreatedAsync(RollResult roll)
{
IsMutating = true;
try
{
await HandleRecordedRollAsync(roll);
}
finally
{
IsMutating = false;
}
}
private async Task HandleRecordedRollAsync(RollResult roll)
{
LastRoll = roll;
CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll);
CampaignLogDetailErrors.Remove(roll.RollId);
await RefreshCampaignLogAsync(CampaignLogCursor);
PromoteFreshRoll(roll.RollId);
ResetCampaignStateTracking();
SetStatus("Roll recorded.", false);
Announce("Roll result updated.");
}
private Task OnCustomRollCreatedAsync(RollResult roll) => Play.OnCustomRollCreatedAsync(roll);
private Task OnRollVisibilityChanged(string visibility) => Session.OnRollVisibilityChangedAsync(visibility);
private bool CanEditSkill(CharacterSheetSkill skill)
{
if (SelectedCharacter is null)
return false;
return CanEditCharacter(SelectedCharacter);
}
private bool CanEditSkill(CharacterSheetSkill skill) => Play.CanEditSkill(skill);
[JSInvokable]
public async Task OnStateEventReceived(CampaignStateSnapshot state)
@@ -571,85 +374,13 @@ public partial class Workspace : IAsyncDisposable
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
}
private CampaignRollDetail? ResolveRollDetail(Guid rollId)
{
return CampaignLogDetails.GetValueOrDefault(rollId);
}
private CampaignRollDetail? ResolveRollDetail(Guid rollId) => Play.ResolveRollDetail(rollId);
private bool IsRollDetailLoading(Guid rollId)
{
return CampaignLogDetailsLoading.Contains(rollId);
}
private bool IsRollDetailLoading(Guid rollId) => Play.IsRollDetailLoading(rollId);
private string? GetRollDetailError(Guid rollId)
{
return CampaignLogDetailErrors.GetValueOrDefault(rollId);
}
private string? GetRollDetailError(Guid rollId) => Play.GetRollDetailError(rollId);
private void ResetCampaignLogDetailState()
{
ExpandedCampaignLogRollId = null;
FreshCampaignLogRollId = null;
CampaignLogDetails.Clear();
CampaignLogDetailsLoading.Clear();
CampaignLogDetailErrors.Clear();
}
private void TrimCampaignLogDetails()
{
var visibleRollIds = CampaignLog.Select(entry => entry.RollId).ToHashSet();
foreach (var rollId in CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
CampaignLogDetails.Remove(rollId);
foreach (var rollId in CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
CampaignLogDetailsLoading.Remove(rollId);
foreach (var rollId in CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
CampaignLogDetailErrors.Remove(rollId);
if (ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(ExpandedCampaignLogRollId.Value))
ExpandedCampaignLogRollId = null;
if (FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(FreshCampaignLogRollId.Value))
FreshCampaignLogRollId = null;
}
private async Task EnsureRollDetailLoadedAsync(Guid rollId)
{
CampaignLogDetailErrors.Remove(rollId);
if (CampaignLogDetails.ContainsKey(rollId) || CampaignLogDetailsLoading.Contains(rollId))
return;
CampaignLogDetailsLoading.Add(rollId);
try
{
CampaignLogDetails[rollId] = await WorkspaceQuery.GetRollDetailAsync(rollId);
}
catch (ApiRequestException ex)
{
CampaignLogDetailErrors[rollId] = ex.Message;
}
finally
{
CampaignLogDetailsLoading.Remove(rollId);
await InvokeAsync(StateHasChanged);
}
}
private static CampaignRollDetail ToCampaignRollDetail(RollResult roll)
{
return new CampaignRollDetail(roll.RollId, roll.Breakdown, roll.Dice.ToArray());
}
private void PromoteFreshRoll(Guid rollId)
{
if (!CampaignLog.Any(entry => entry.RollId == rollId))
return;
ExpandedCampaignLogRollId = rollId;
FreshCampaignLogRollId = rollId;
}
private void ResetCampaignLogDetailState() => Play.ResetCampaignLogDetailState();
private void ClearAuthenticatedState() => Session.ClearAuthenticatedState();
@@ -668,10 +399,7 @@ public partial class Workspace : IAsyncDisposable
IsScreenMenuOpen = !IsScreenMenuOpen;
}
private void ResetCampaignStateTracking()
{
CurrentCampaignState = null;
}
private void ResetCampaignStateTracking() => Play.ResetCampaignStateTracking();
private static long GetCharacterVersion(CampaignStateSnapshot snapshot, Guid? characterId)
{
@@ -764,6 +492,13 @@ public partial class Workspace : IAsyncDisposable
private bool IsPlayScreen => State.IsPlayScreen;
private bool IsManagementScreen => State.IsManagementScreen;
private bool IsAdminScreen => State.IsAdminScreen;
private WorkspacePlayCoordinator Play => m_Play ??= new(
State,
Feedback,
ApiClient,
WorkspaceQuery,
CanEditCharacter,
() => InvokeAsync(StateHasChanged));
private WorkspaceCampaignCoordinator CampaignsFlow => m_CampaignsFlow ??= new(
State,
Feedback,
@@ -826,6 +561,7 @@ public partial class Workspace : IAsyncDisposable
private const string MobilePanelSessionKey = "play-panel";
private const int CampaignLogWindowSize = 25;
private WorkspacePlayCoordinator? m_Play;
private WorkspaceCampaignCoordinator? m_CampaignsFlow;
private WorkspaceAdminCoordinator? m_Admin;
private WorkspaceFeedbackService? m_Feedback;

View File

@@ -0,0 +1,352 @@
using System.Diagnostics.CodeAnalysis;
using RpgRoller.Contracts;
namespace RpgRoller.Components.Pages;
[ExcludeFromCodeCoverage]
public sealed class WorkspacePlayCoordinator
{
public WorkspacePlayCoordinator(
WorkspaceState state,
WorkspaceFeedbackService feedback,
RpgRollerApiClient apiClient,
WorkspaceQueryService workspaceQuery,
Func<CharacterSummary, bool> canEditCharacter,
Func<Task> requestRefreshAsync)
{
m_State = state;
m_Feedback = feedback;
m_ApiClient = apiClient;
m_WorkspaceQuery = workspaceQuery;
m_CanEditCharacter = canEditCharacter;
m_RequestRefreshAsync = requestRefreshAsync;
}
public async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
{
if (!m_State.SelectedCampaignId.HasValue || !m_State.IsPlayScreen)
{
m_State.CampaignLog = [];
m_State.CampaignLogCursor = null;
ResetCampaignLogDetailState();
return;
}
var previousLogCount = m_State.CampaignLog.Count;
var page = await m_WorkspaceQuery.GetCampaignLogPageAsync(m_State.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
Guid? newestRollId = null;
if (!afterRollId.HasValue || page.ResetRequired)
{
m_State.CampaignLog = page.Entries.ToList();
}
else if (page.Entries.Length > 0)
{
m_State.CampaignLog.AddRange(page.Entries);
if (m_State.CampaignLog.Count > CampaignLogWindowSize)
m_State.CampaignLog = m_State.CampaignLog.TakeLast(CampaignLogWindowSize).ToList();
}
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
if (!shouldAutoExpandNewest &&
!afterRollId.HasValue &&
m_State.CurrentCampaignState is not null &&
previousLogCount == 0 &&
page.Entries.Length > 0)
{
shouldAutoExpandNewest = true;
}
if (shouldAutoExpandNewest)
{
newestRollId = page.Entries[^1].RollId;
m_State.ExpandedCampaignLogRollId = newestRollId;
m_State.FreshCampaignLogRollId = newestRollId;
}
else if (!afterRollId.HasValue)
{
m_State.FreshCampaignLogRollId = null;
}
m_State.CampaignLogCursor = page.Cursor ?? afterRollId;
TrimCampaignLogDetails();
if (newestRollId.HasValue)
await EnsureRollDetailLoadedAsync(newestRollId.Value);
}
public async Task SelectCharacterAsync(Guid characterId)
{
m_State.SelectedCharacterId = characterId;
await RefreshSelectedCharacterSheetAsync();
await EnsureSelectedCharacterActiveAsync();
}
public async Task RefreshSelectedCharacterSheetAsync()
{
if (!m_State.SelectedCharacterId.HasValue || m_State.SelectedCampaign is null || !m_State.IsPlayScreen)
{
m_State.SelectedCharacterSkills = [];
m_State.SelectedCharacterSkillGroups = [];
return;
}
var sheet = await m_WorkspaceQuery.GetCharacterSheetAsync(m_State.SelectedCharacterId.Value);
m_State.SelectedCharacterSkillGroups = sheet.SkillGroups
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
m_State.SelectedCharacterSkills = sheet.Skills
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public Task EnsureSelectedCharacterActiveAsync()
{
return EnsureSelectedCharacterActiveCoreAsync();
}
public async Task ToggleRollDetailAsync(Guid rollId)
{
if (m_State.ExpandedCampaignLogRollId == rollId)
{
m_State.ExpandedCampaignLogRollId = null;
if (m_State.FreshCampaignLogRollId == rollId)
m_State.FreshCampaignLogRollId = null;
return;
}
m_State.ExpandedCampaignLogRollId = rollId;
m_State.FreshCampaignLogRollId = null;
await EnsureRollDetailLoadedAsync(rollId);
}
public async Task OnSkillCreatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
m_Feedback.SetStatus("Skill created.", false);
}
public async Task OnSkillUpdatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
m_Feedback.SetStatus("Skill updated.", false);
}
public async Task OnSkillGroupCreatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
m_Feedback.SetStatus("Skill group created.", false);
}
public async Task OnSkillGroupUpdatedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
m_Feedback.SetStatus("Skill group updated.", false);
}
public async Task OnSkillDeletedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
m_Feedback.SetStatus("Skill deleted.", false);
}
public async Task OnSkillGroupDeletedAsync(Guid _)
{
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
m_Feedback.SetStatus("Skill group deleted.", false);
}
public Task OnCharacterPanelErrorAsync(string message)
{
m_Feedback.SetStatus(message, true);
return Task.CompletedTask;
}
public Task OnCampaignLogPanelErrorAsync(string message)
{
m_Feedback.SetStatus(message, true);
return Task.CompletedTask;
}
public async Task RollSkillAsync(Guid skillId)
{
if (m_State.SelectedCampaign is null)
{
m_Feedback.SetStatus("No campaign selected.", true);
return;
}
m_State.IsMutating = true;
try
{
var roll = await m_ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(m_State.RollVisibility));
await HandleRecordedRollAsync(roll);
}
catch (ApiRequestException ex)
{
m_Feedback.SetStatus(ex.Message, true);
}
finally
{
m_State.IsMutating = false;
}
}
public async Task OnCustomRollCreatedAsync(RollResult roll)
{
m_State.IsMutating = true;
try
{
await HandleRecordedRollAsync(roll);
}
finally
{
m_State.IsMutating = false;
}
}
public bool CanEditSkill(CharacterSheetSkill skill)
{
if (m_State.SelectedCharacter is null)
return false;
return m_CanEditCharacter(m_State.SelectedCharacter);
}
public CampaignRollDetail? ResolveRollDetail(Guid rollId)
{
return m_State.CampaignLogDetails.GetValueOrDefault(rollId);
}
public bool IsRollDetailLoading(Guid rollId)
{
return m_State.CampaignLogDetailsLoading.Contains(rollId);
}
public string? GetRollDetailError(Guid rollId)
{
return m_State.CampaignLogDetailErrors.GetValueOrDefault(rollId);
}
public void ResetCampaignLogDetailState()
{
m_State.ExpandedCampaignLogRollId = null;
m_State.FreshCampaignLogRollId = null;
m_State.CampaignLogDetails.Clear();
m_State.CampaignLogDetailsLoading.Clear();
m_State.CampaignLogDetailErrors.Clear();
}
public void ResetCampaignStateTracking()
{
m_State.CurrentCampaignState = null;
}
private async Task EnsureSelectedCharacterActiveCoreAsync()
{
if (!m_State.SelectedCharacterId.HasValue || m_State.SelectedCampaign is null)
return;
var character = m_State.SelectedCampaign.Characters.FirstOrDefault(c => c.Id == m_State.SelectedCharacterId.Value);
if (character is null || !CanActivateCharacter(character, m_State.User) || m_State.ActiveCharacterId == character.Id)
return;
try
{
await m_ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
m_State.ActiveCharacterId = character.Id;
}
catch (ApiRequestException ex)
{
m_Feedback.SetStatus(ex.Message, true);
}
}
private async Task HandleRecordedRollAsync(RollResult roll)
{
m_State.LastRoll = roll;
m_State.CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll);
m_State.CampaignLogDetailErrors.Remove(roll.RollId);
await RefreshCampaignLogAsync(m_State.CampaignLogCursor);
PromoteFreshRoll(roll.RollId);
ResetCampaignStateTracking();
m_Feedback.SetStatus("Roll recorded.", false);
m_Feedback.Announce("Roll result updated.");
}
private void TrimCampaignLogDetails()
{
var visibleRollIds = m_State.CampaignLog.Select(entry => entry.RollId).ToHashSet();
foreach (var rollId in m_State.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
m_State.CampaignLogDetails.Remove(rollId);
foreach (var rollId in m_State.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
m_State.CampaignLogDetailsLoading.Remove(rollId);
foreach (var rollId in m_State.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
m_State.CampaignLogDetailErrors.Remove(rollId);
if (m_State.ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(m_State.ExpandedCampaignLogRollId.Value))
m_State.ExpandedCampaignLogRollId = null;
if (m_State.FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(m_State.FreshCampaignLogRollId.Value))
m_State.FreshCampaignLogRollId = null;
}
private async Task EnsureRollDetailLoadedAsync(Guid rollId)
{
m_State.CampaignLogDetailErrors.Remove(rollId);
if (m_State.CampaignLogDetails.ContainsKey(rollId) || m_State.CampaignLogDetailsLoading.Contains(rollId))
return;
m_State.CampaignLogDetailsLoading.Add(rollId);
try
{
m_State.CampaignLogDetails[rollId] = await m_WorkspaceQuery.GetRollDetailAsync(rollId);
}
catch (ApiRequestException ex)
{
m_State.CampaignLogDetailErrors[rollId] = ex.Message;
}
finally
{
m_State.CampaignLogDetailsLoading.Remove(rollId);
await m_RequestRefreshAsync();
}
}
private void PromoteFreshRoll(Guid rollId)
{
if (!m_State.CampaignLog.Any(entry => entry.RollId == rollId))
return;
m_State.ExpandedCampaignLogRollId = rollId;
m_State.FreshCampaignLogRollId = rollId;
}
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
{
return user is not null && character.OwnerUserId == user.Id;
}
private static CampaignRollDetail ToCampaignRollDetail(RollResult roll)
{
return new CampaignRollDetail(roll.RollId, roll.Breakdown, roll.Dice.ToArray());
}
private readonly RpgRollerApiClient m_ApiClient;
private readonly Func<CharacterSummary, bool> m_CanEditCharacter;
private readonly WorkspaceFeedbackService m_Feedback;
private readonly Func<Task> m_RequestRefreshAsync;
private readonly WorkspaceState m_State;
private readonly WorkspaceQueryService m_WorkspaceQuery;
private const int CampaignLogWindowSize = 25;
}