Add targeted workspace live refresh

This commit is contained in:
2026-04-01 23:50:01 +02:00
parent 107b8b8552
commit 6ea91ee565
10 changed files with 281 additions and 60 deletions

View File

@@ -153,6 +153,35 @@ public partial class Workspace : IAsyncDisposable
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 async Task RefreshCampaignLogAsync()
{
if (!SelectedCampaignId.HasValue || !IsPlayScreen)
{
CampaignLog = [];
return;
}
CampaignLog = (await WorkspaceQuery.GetCampaignLogAsync(SelectedCampaignId.Value)).ToList();
}
private async Task RefreshCampaignScopeAsync()
{
if (!SelectedCampaignId.HasValue)
@@ -163,26 +192,17 @@ public partial class Workspace : IAsyncDisposable
CampaignLog = [];
SelectedCharacterId = null;
ConnectionState = "offline";
CurrentCampaignState = null;
return;
}
IsCampaignDataLoading = true;
try
{
var campaignId = SelectedCampaignId.Value;
SelectedCampaign = await WorkspaceQuery.GetCampaignAsync(campaignId);
SyncSelectedCharacter();
if (IsPlayScreen && PlaySelectedCharacterId.HasValue && SelectedCharacterId != PlaySelectedCharacterId)
SelectedCharacterId = PlaySelectedCharacterId;
await RefreshCampaignRosterAsync();
await RefreshSelectedCharacterSheetAsync();
CampaignLog = IsPlayScreen
? (await WorkspaceQuery.GetCampaignLogAsync(campaignId)).ToList()
: [];
await EnsureSelectedCharacterActiveAsync();
await RefreshCampaignLogAsync();
ResetCampaignStateTracking();
}
catch (ApiRequestException ex) when (ex.StatusCode == 401)
{
@@ -582,37 +602,43 @@ public partial class Workspace : IAsyncDisposable
private async Task OnSkillCreatedAsync(Guid _)
{
await RefreshCampaignScopeAsync();
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill created.", false);
}
private async Task OnSkillUpdatedAsync(Guid _)
{
await RefreshCampaignScopeAsync();
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill updated.", false);
}
private async Task OnSkillGroupCreatedAsync(Guid _)
{
await RefreshCampaignScopeAsync();
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill group created.", false);
}
private async Task OnSkillGroupUpdatedAsync(Guid _)
{
await RefreshCampaignScopeAsync();
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill group updated.", false);
}
private async Task OnSkillDeletedAsync(Guid _)
{
await RefreshCampaignScopeAsync();
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill deleted.", false);
}
private async Task OnSkillGroupDeletedAsync(Guid _)
{
await RefreshCampaignScopeAsync();
await RefreshSelectedCharacterSheetAsync();
ResetCampaignStateTracking();
SetStatus("Skill group deleted.", false);
}
@@ -635,7 +661,8 @@ public partial class Workspace : IAsyncDisposable
{
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
await RefreshCampaignScopeAsync();
await RefreshCampaignLogAsync();
ResetCampaignStateTracking();
SetStatus("Roll recorded.", false);
Announce("Roll result updated.");
}
@@ -698,15 +725,44 @@ public partial class Workspace : IAsyncDisposable
}
[JSInvokable]
public async Task OnStateEventReceived(long _)
public async Task OnStateEventReceived(CampaignStateSnapshot state)
{
if (StateRefreshInProgress)
return;
if (!SelectedCampaignId.HasValue || state.CampaignId != SelectedCampaignId.Value)
return;
StateRefreshInProgress = true;
try
{
await RefreshCampaignScopeAsync();
if (CurrentCampaignState is null)
{
CurrentCampaignState = state;
return;
}
var previousState = CurrentCampaignState;
var previousSelectedCharacterId = SelectedCharacterId;
var previousSelectedCharacterVersion = GetCharacterVersion(previousState, previousSelectedCharacterId);
var rosterChanged = state.RosterVersion != previousState.RosterVersion;
var logChanged = IsPlayScreen && state.LogVersion != previousState.LogVersion;
if (rosterChanged)
await RefreshCampaignRosterAsync();
var selectedCharacterChanged = previousSelectedCharacterId != SelectedCharacterId;
var selectedCharacterVersionChanged = IsPlayScreen &&
!selectedCharacterChanged &&
GetCharacterVersion(state, SelectedCharacterId) != previousSelectedCharacterVersion;
if (IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged))
await RefreshSelectedCharacterSheetAsync();
if (logChanged)
await RefreshCampaignLogAsync();
CurrentCampaignState = state;
}
finally
{
@@ -935,6 +991,21 @@ public partial class Workspace : IAsyncDisposable
IsScreenMenuOpen = !IsScreenMenuOpen;
}
private void ResetCampaignStateTracking()
{
CurrentCampaignState = null;
}
private static long GetCharacterVersion(CampaignStateSnapshot snapshot, Guid? characterId)
{
if (!characterId.HasValue)
return 0;
return snapshot.CharacterVersions
.FirstOrDefault(version => version.CharacterId == characterId.Value)
?.Version ?? 0;
}
[Inject]
private IJSRuntime JS { get; set; } = null!;
@@ -987,6 +1058,7 @@ public partial class Workspace : IAsyncDisposable
private bool StateRefreshInProgress { get; set; }
private bool HasInteractiveRenderStarted { get; set; }
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
private CampaignStateSnapshot? CurrentCampaignState { get; set; }
[Parameter]
public EventCallback<string?> LoggedOut { get; set; }