841 lines
27 KiB
C#
841 lines
27 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.JSInterop;
|
|
using RpgRoller.Contracts;
|
|
using RpgRoller.Domain;
|
|
|
|
namespace RpgRoller.Components.Pages;
|
|
|
|
[ExcludeFromCodeCoverage]
|
|
public partial class Workspace : IAsyncDisposable
|
|
{
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
HasInteractiveRenderStarted = true;
|
|
if (!firstRender)
|
|
return;
|
|
|
|
await InitializeAsync();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task InitializeAsync()
|
|
{
|
|
var storedScreen = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", ScreenSessionKey);
|
|
if (string.Equals(storedScreen, "management", StringComparison.OrdinalIgnoreCase))
|
|
CurrentScreen = "management";
|
|
|
|
var storedPanel = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", MobilePanelSessionKey);
|
|
if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase))
|
|
MobilePanel = "log";
|
|
|
|
var storedRollVisibility = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", RollVisibilitySessionKey);
|
|
RollVisibility = NormalizeRollVisibility(storedRollVisibility);
|
|
|
|
Guid? preferredCampaignId = null;
|
|
var storedCampaignId = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", CampaignSessionKey);
|
|
if (Guid.TryParse(storedCampaignId, out var parsedCampaignId))
|
|
preferredCampaignId = parsedCampaignId;
|
|
|
|
await CheckHealthAsync();
|
|
await LoadRulesetsAsync();
|
|
|
|
var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId);
|
|
if (!reloaded)
|
|
await LoggedOut.InvokeAsync("Session expired. Please log in again.");
|
|
}
|
|
|
|
private async Task RetryAfterHealthIssueAsync()
|
|
{
|
|
await CheckHealthAsync();
|
|
if (!HasHealthIssue && User is not null)
|
|
{
|
|
var reloaded = await ReloadAuthenticatedSessionAsync(SelectedCampaignId);
|
|
if (!reloaded)
|
|
await LoggedOut.InvokeAsync("Session expired. Please log in again.");
|
|
}
|
|
}
|
|
|
|
private async Task CheckHealthAsync()
|
|
{
|
|
try
|
|
{
|
|
var health = await ApiClient.RequestAsync<HealthResponse>("GET", "/api/health");
|
|
if (!string.Equals(health.Status, "ok", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
HasHealthIssue = true;
|
|
HealthIssueMessage = "Health endpoint returned an unhealthy response.";
|
|
return;
|
|
}
|
|
|
|
HasHealthIssue = false;
|
|
HealthIssueMessage = string.Empty;
|
|
}
|
|
catch (ApiRequestException)
|
|
{
|
|
HasHealthIssue = true;
|
|
HealthIssueMessage = "Unable to reach API. Retry to continue.";
|
|
}
|
|
}
|
|
|
|
private async Task LoadRulesetsAsync()
|
|
{
|
|
try
|
|
{
|
|
Rulesets = (await ApiClient.RequestAsync<IReadOnlyList<RulesetDefinition>>("GET", "/api/rulesets")).ToList();
|
|
}
|
|
catch (ApiRequestException ex)
|
|
{
|
|
SetStatus(ex.Message, true);
|
|
}
|
|
}
|
|
|
|
private async Task LoadKnownUsernamesAsync()
|
|
{
|
|
try
|
|
{
|
|
var usernames = await ApiClient.RequestAsync<IReadOnlyList<string>>("GET", "/api/users/usernames");
|
|
KnownUsernames = usernames.OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToList();
|
|
}
|
|
catch (ApiRequestException ex)
|
|
{
|
|
KnownUsernames = [];
|
|
SetStatus(ex.Message, true);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> ReloadAuthenticatedSessionAsync(Guid? preferredCampaignId)
|
|
{
|
|
var me = await TryGetMeAsync();
|
|
if (me is null)
|
|
{
|
|
ClearAuthenticatedState();
|
|
await StopStateEventsAsync();
|
|
return false;
|
|
}
|
|
|
|
User = me.User;
|
|
ActiveCharacterId = me.ActiveCharacterId;
|
|
|
|
await ReloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
|
await ReloadCharacterCampaignOptionsAsync();
|
|
await RefreshCampaignScopeAsync();
|
|
await SyncStateEventsAsync();
|
|
return true;
|
|
}
|
|
|
|
private async Task<MeResponse?> TryGetMeAsync()
|
|
{
|
|
try
|
|
{
|
|
return await ApiClient.RequestAsync<MeResponse>("GET", "/api/me");
|
|
}
|
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
|
{
|
|
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignDetails>>("GET", "/api/campaigns");
|
|
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
|
|
if (Campaigns.Count == 0)
|
|
{
|
|
SelectedCampaignId = null;
|
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null);
|
|
return;
|
|
}
|
|
|
|
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 ApiClient.RequestAsync<IReadOnlyList<CampaignOption>>("GET", "/api/campaigns/options");
|
|
CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
}
|
|
|
|
private async Task RefreshCampaignScopeAsync()
|
|
{
|
|
if (!SelectedCampaignId.HasValue)
|
|
{
|
|
SelectedCampaign = null;
|
|
CampaignLog = [];
|
|
SelectedCharacterId = null;
|
|
ConnectionState = "offline";
|
|
return;
|
|
}
|
|
|
|
IsCampaignDataLoading = true;
|
|
try
|
|
{
|
|
var campaignId = SelectedCampaignId.Value;
|
|
SelectedCampaign = await ApiClient.RequestAsync<CampaignDetails>("GET", $"/api/campaigns/{campaignId}");
|
|
CampaignLog = (await ApiClient.RequestAsync<IReadOnlyList<CampaignLogEntry>>("GET", $"/api/campaigns/{campaignId}/log")).ToList();
|
|
SyncSelectedCharacter();
|
|
await EnsureSelectedCharacterActiveAsync();
|
|
}
|
|
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 async Task LogoutAsync()
|
|
{
|
|
if (IsMutating)
|
|
return;
|
|
|
|
IsMutating = true;
|
|
try
|
|
{
|
|
await ApiClient.RequestWithoutPayloadAsync("POST", "/api/auth/logout");
|
|
}
|
|
catch (ApiRequestException)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
IsMutating = false;
|
|
}
|
|
|
|
ClearAuthenticatedState();
|
|
await StopStateEventsAsync();
|
|
await LoggedOut.InvokeAsync("Logged out.");
|
|
}
|
|
|
|
private async Task SwitchScreenAsync(string screen)
|
|
{
|
|
CurrentScreen = string.Equals(screen, "management", StringComparison.OrdinalIgnoreCase) ? "management" : "play";
|
|
IsScreenMenuOpen = false;
|
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, CurrentScreen);
|
|
}
|
|
|
|
private Task SwitchToPlayAsync()
|
|
{
|
|
return SwitchScreenAsync("play");
|
|
}
|
|
|
|
private Task SwitchToManagementAsync()
|
|
{
|
|
return SwitchScreenAsync("management");
|
|
}
|
|
|
|
private async Task OpenAdminAsync()
|
|
{
|
|
IsScreenMenuOpen = false;
|
|
await AdminRequested.InvokeAsync();
|
|
}
|
|
|
|
private async Task SetMobilePanelAsync(string panel)
|
|
{
|
|
MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
|
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, MobilePanel);
|
|
}
|
|
|
|
private Task SetMobilePanelCharacterAsync()
|
|
{
|
|
return SetMobilePanelAsync("character");
|
|
}
|
|
|
|
private Task SetMobilePanelLogAsync()
|
|
{
|
|
return SetMobilePanelAsync("log");
|
|
}
|
|
|
|
private async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args)
|
|
{
|
|
if (!Guid.TryParse(args.Value?.ToString(), out var campaignId))
|
|
return;
|
|
|
|
SelectedCampaignId = campaignId;
|
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString());
|
|
await RefreshCampaignScopeAsync();
|
|
await SyncStateEventsAsync();
|
|
IsScreenMenuOpen = false;
|
|
}
|
|
|
|
private async Task OnCampaignCreatedAsync(Guid campaignId)
|
|
{
|
|
await ReloadCampaignsAsync(campaignId);
|
|
await ReloadCharacterCampaignOptionsAsync();
|
|
await RefreshCampaignScopeAsync();
|
|
await SyncStateEventsAsync();
|
|
SetStatus("Campaign created.", false);
|
|
}
|
|
|
|
private void OpenCreateCharacterModal()
|
|
{
|
|
CreateCharacterInitialModel = new()
|
|
{
|
|
Name = string.Empty,
|
|
CampaignId = SelectedCampaignId?.ToString() ?? CharacterCampaignOptions.FirstOrDefault()?.Id.ToString() ?? string.Empty,
|
|
OwnerUsername = string.Empty
|
|
};
|
|
|
|
CreateCharacterFormVersion++;
|
|
CanEditCharacterOwner = false;
|
|
ShowCreateCharacterModal = true;
|
|
}
|
|
|
|
private async Task OpenEditCharacterModal(CharacterSummary character)
|
|
{
|
|
if (IsCurrentUserGm || IsCurrentUserAdmin)
|
|
await LoadKnownUsernamesAsync();
|
|
|
|
EditingCharacterId = character.Id;
|
|
EditCharacterInitialModel = new()
|
|
{
|
|
Name = character.Name,
|
|
CampaignId = character.CampaignId?.ToString() ?? string.Empty,
|
|
OwnerUsername = string.Empty
|
|
};
|
|
|
|
EditCharacterFormVersion++;
|
|
CanEditCharacterOwner = IsCurrentUserGm || IsCurrentUserAdmin;
|
|
ShowEditCharacterModal = true;
|
|
}
|
|
|
|
private void CloseCharacterModals()
|
|
{
|
|
ShowCreateCharacterModal = false;
|
|
ShowEditCharacterModal = false;
|
|
CanEditCharacterOwner = false;
|
|
EditingCharacterId = null;
|
|
}
|
|
|
|
private async Task OnCharacterCreatedAsync(Guid? campaignId)
|
|
{
|
|
CloseCharacterModals();
|
|
await ReloadCampaignsAsync(campaignId);
|
|
await ReloadCharacterCampaignOptionsAsync();
|
|
await RefreshCampaignScopeAsync();
|
|
await SyncStateEventsAsync();
|
|
SetStatus("Character created.", false);
|
|
}
|
|
|
|
private async Task OnCharacterUpdatedAsync(Guid? campaignId)
|
|
{
|
|
CloseCharacterModals();
|
|
await ReloadCampaignsAsync(campaignId);
|
|
await ReloadCharacterCampaignOptionsAsync();
|
|
await RefreshCampaignScopeAsync();
|
|
await SyncStateEventsAsync();
|
|
SetStatus(campaignId.HasValue ? "Character updated." : "Character unlinked from campaign.", false);
|
|
}
|
|
|
|
private async Task DeleteSelectedCampaignAsync()
|
|
{
|
|
if (SelectedCampaign is null || IsMutating || !CanDeleteSelectedCampaign)
|
|
return;
|
|
|
|
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Delete campaign '{SelectedCampaign.Name}'?");
|
|
if (!confirmed)
|
|
return;
|
|
|
|
IsMutating = true;
|
|
try
|
|
{
|
|
_ = await ApiClient.RequestAsync<bool>("DELETE", $"/api/campaigns/{SelectedCampaign.Id}");
|
|
await ReloadCampaignsAsync(null);
|
|
await ReloadCharacterCampaignOptionsAsync();
|
|
await RefreshCampaignScopeAsync();
|
|
await SyncStateEventsAsync();
|
|
SetStatus("Campaign deleted.", false);
|
|
}
|
|
catch (ApiRequestException ex)
|
|
{
|
|
SetStatus(ex.Message, true);
|
|
}
|
|
finally
|
|
{
|
|
IsMutating = false;
|
|
}
|
|
}
|
|
|
|
private async Task SelectCharacterAsync(Guid characterId)
|
|
{
|
|
SelectedCharacterId = characterId;
|
|
await EnsureSelectedCharacterActiveAsync();
|
|
}
|
|
|
|
private bool CanEditCharacter(CharacterSummary character)
|
|
{
|
|
return User is not null && (character.OwnerUserId == User.Id || IsCurrentUserGm || IsCurrentUserAdmin);
|
|
}
|
|
|
|
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
|
|
{
|
|
return user is not null && character.OwnerUserId == user.Id;
|
|
}
|
|
|
|
private async Task EnsureSelectedCharacterActiveAsync()
|
|
{
|
|
if (!SelectedCharacterId.HasValue || SelectedCampaign is null)
|
|
return;
|
|
|
|
var character = SelectedCampaign.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId.Value);
|
|
if (character is null || !CanActivateCharacter(character, User) || ActiveCharacterId == character.Id)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
|
|
ActiveCharacterId = character.Id;
|
|
}
|
|
catch (ApiRequestException ex)
|
|
{
|
|
SetStatus(ex.Message, true);
|
|
}
|
|
}
|
|
|
|
private async Task OnSkillCreatedAsync(Guid _)
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Skill created.", false);
|
|
}
|
|
|
|
private async Task OnSkillUpdatedAsync(Guid _)
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Skill updated.", false);
|
|
}
|
|
|
|
private async Task OnSkillGroupCreatedAsync(Guid _)
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Skill group created.", false);
|
|
}
|
|
|
|
private async Task OnSkillGroupUpdatedAsync(Guid _)
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Skill group updated.", false);
|
|
}
|
|
|
|
private async Task OnSkillDeletedAsync(Guid _)
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Skill deleted.", false);
|
|
}
|
|
|
|
private async Task OnSkillGroupDeletedAsync(Guid _)
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Skill group deleted.", false);
|
|
}
|
|
|
|
private Task OnCharacterPanelErrorAsync(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
|
|
{
|
|
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
|
|
|
|
await RefreshCampaignScopeAsync();
|
|
SetStatus("Roll recorded.", false);
|
|
Announce("Roll result updated.");
|
|
}
|
|
catch (ApiRequestException ex)
|
|
{
|
|
SetStatus(ex.Message, true);
|
|
}
|
|
finally
|
|
{
|
|
IsMutating = false;
|
|
}
|
|
}
|
|
|
|
private async Task OnRollVisibilityChanged(string visibility)
|
|
{
|
|
RollVisibility = NormalizeRollVisibility(visibility);
|
|
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, RollVisibility);
|
|
}
|
|
|
|
private static string NormalizeRollVisibility(string? visibility)
|
|
{
|
|
return string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public";
|
|
}
|
|
|
|
private bool CanEditSkill(SkillSummary skill)
|
|
{
|
|
if (SelectedCampaign is null)
|
|
return false;
|
|
|
|
var character = SelectedCampaign.Characters.FirstOrDefault(c => c.Id == skill.CharacterId);
|
|
return character is not null && CanEditCharacter(character);
|
|
}
|
|
|
|
[JSInvokable]
|
|
public async Task OnStateEventReceived(long _)
|
|
{
|
|
if (StateRefreshInProgress)
|
|
return;
|
|
|
|
StateRefreshInProgress = true;
|
|
try
|
|
{
|
|
await RefreshCampaignScopeAsync();
|
|
}
|
|
finally
|
|
{
|
|
StateRefreshInProgress = false;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
[JSInvokable]
|
|
public Task OnConnectionStateChanged(string state)
|
|
{
|
|
ConnectionState = state switch
|
|
{
|
|
"connected" => "connected",
|
|
"reconnecting" => "reconnecting",
|
|
_ => "offline"
|
|
};
|
|
|
|
if (ConnectionState == "reconnecting")
|
|
Announce("Reconnecting to live updates.");
|
|
|
|
if (ConnectionState == "offline")
|
|
Announce("Live updates offline. Use manual refresh.");
|
|
|
|
return InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task SyncStateEventsAsync()
|
|
{
|
|
if (User is null || !SelectedCampaignId.HasValue)
|
|
{
|
|
await StopStateEventsAsync();
|
|
ConnectionState = "offline";
|
|
return;
|
|
}
|
|
|
|
DotNetRef ??= DotNetObjectReference.Create(this);
|
|
await JS.InvokeVoidAsync("rpgRollerApi.startStateEvents", SelectedCampaignId.Value.ToString(), DotNetRef);
|
|
ConnectionState = "reconnecting";
|
|
}
|
|
|
|
private async Task StopStateEventsAsync()
|
|
{
|
|
if (!HasInteractiveRenderStarted)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await JS.InvokeVoidAsync("rpgRollerApi.stopStateEvents");
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
catch (InvalidOperationException ex) when (IsStaticRenderInteropException(ex))
|
|
{
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await StopStateEventsAsync();
|
|
DotNetRef?.Dispose();
|
|
}
|
|
|
|
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
|
|
{
|
|
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private void SyncSelectedCharacter()
|
|
{
|
|
if (SelectedCampaign is null || SelectedCampaign.Characters.Count == 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)
|
|
return "You";
|
|
|
|
if (SelectedCampaign is null)
|
|
return "Unknown owner";
|
|
|
|
if (ownerUserId == SelectedCampaign.Gm.Id)
|
|
return $"{SelectedCampaign.Gm.DisplayName} (GM)";
|
|
|
|
var ownerDisplayName = SelectedCampaign.Characters
|
|
.Where(character => character.OwnerUserId == ownerUserId)
|
|
.Select(character => character.OwnerDisplayName)
|
|
.FirstOrDefault(displayName => !string.IsNullOrWhiteSpace(displayName));
|
|
|
|
return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName;
|
|
}
|
|
|
|
private string CharacterLabel(Guid characterId)
|
|
{
|
|
return SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == characterId)?.Name ?? "Hidden character";
|
|
}
|
|
|
|
private string SkillLabel(Guid skillId)
|
|
{
|
|
return SelectedCampaign?.Skills.FirstOrDefault(s => s.Id == skillId)?.Name ?? "Hidden skill";
|
|
}
|
|
|
|
private string SkillDefinitionLabel(SkillSummary skill)
|
|
{
|
|
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
|
return skill.DiceRollDefinition;
|
|
|
|
var fumbleLabel = skill.AllowFumble ? "fumble on" : "fumble off";
|
|
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
|
}
|
|
|
|
private string RollerLabel(CampaignLogEntry entry)
|
|
{
|
|
if (User is not null && entry.RollerUserId == User.Id)
|
|
return "You";
|
|
|
|
if (SelectedCampaign is not null && entry.RollerUserId == SelectedCampaign.Gm.Id)
|
|
return "GM";
|
|
|
|
return "Participant";
|
|
}
|
|
|
|
private string VisibilityLabel(CampaignLogEntry entry)
|
|
{
|
|
if (!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase))
|
|
return "Public";
|
|
|
|
if (User is not null && entry.RollerUserId == User.Id)
|
|
return "Private (you)";
|
|
|
|
return IsCurrentUserGm ? "Private (GM view)" : "Private";
|
|
}
|
|
|
|
private string VisibilityBadgeCssClass(CampaignLogEntry entry)
|
|
{
|
|
if (!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase))
|
|
return "public";
|
|
|
|
if (User is not null && entry.RollerUserId == User.Id)
|
|
return "private-self";
|
|
|
|
return IsCurrentUserGm ? "private-gm" : "private-generic";
|
|
}
|
|
|
|
private string LogEntryCssClass(CampaignLogEntry entry)
|
|
{
|
|
if (!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase))
|
|
return "public";
|
|
|
|
if (User is not null && entry.RollerUserId == User.Id)
|
|
return "private-self";
|
|
|
|
return IsCurrentUserGm ? "private-gm" : "private-generic";
|
|
}
|
|
|
|
private void ClearAuthenticatedState()
|
|
{
|
|
User = null;
|
|
ActiveCharacterId = null;
|
|
SelectedCampaignId = null;
|
|
SelectedCampaign = null;
|
|
Campaigns = [];
|
|
CharacterCampaignOptions = [];
|
|
CampaignLog = [];
|
|
SelectedCharacterId = null;
|
|
LastRoll = null;
|
|
KnownUsernames = [];
|
|
ShowCreateCharacterModal = false;
|
|
ShowEditCharacterModal = false;
|
|
CanEditCharacterOwner = false;
|
|
CreateCharacterInitialModel = new();
|
|
EditCharacterInitialModel = new();
|
|
CreateCharacterFormVersion = 0;
|
|
EditCharacterFormVersion = 0;
|
|
Toasts.Clear();
|
|
}
|
|
|
|
private void SetStatus(string message, bool isError)
|
|
{
|
|
AddToast(message, isError);
|
|
Announce(message);
|
|
}
|
|
|
|
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)
|
|
{
|
|
}
|
|
}
|
|
|
|
private void ToggleScreenMenu()
|
|
{
|
|
IsScreenMenuOpen = !IsScreenMenuOpen;
|
|
}
|
|
|
|
[Inject]
|
|
private IJSRuntime JS { get; set; } = null!;
|
|
|
|
[Inject]
|
|
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
|
|
|
private UserSummary? User { get; set; }
|
|
private Guid? ActiveCharacterId { get; set; }
|
|
private Guid? SelectedCampaignId { get; set; }
|
|
private CampaignDetails? SelectedCampaign { get; set; }
|
|
private List<CampaignDetails> Campaigns { get; set; } = [];
|
|
private List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
|
|
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
|
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
|
private Guid? SelectedCharacterId { get; set; }
|
|
private RollResult? LastRoll { get; set; }
|
|
private List<string> KnownUsernames { get; set; } = [];
|
|
private string RollVisibility { get; set; } = "public";
|
|
|
|
private bool IsMutating { get; set; }
|
|
private bool IsCampaignDataLoading { get; set; }
|
|
private bool HasHealthIssue { get; set; }
|
|
private string HealthIssueMessage { get; set; } = "Retry to restore the API connection.";
|
|
private List<WorkspaceToast> Toasts { get; } = [];
|
|
private string CurrentScreen { get; set; } = "play";
|
|
private string MobilePanel { get; set; } = "character";
|
|
private string ConnectionState { get; set; } = "offline";
|
|
private string LiveAnnouncement { get; set; } = string.Empty;
|
|
private bool IsScreenMenuOpen { get; set; }
|
|
|
|
private bool ShowCreateCharacterModal { get; set; }
|
|
private bool ShowEditCharacterModal { get; set; }
|
|
private bool CanEditCharacterOwner { get; set; }
|
|
private Guid? EditingCharacterId { get; set; }
|
|
private CharacterFormModel CreateCharacterInitialModel { get; set; } = new();
|
|
private CharacterFormModel EditCharacterInitialModel { get; set; } = new();
|
|
private int CreateCharacterFormVersion { get; set; }
|
|
private int EditCharacterFormVersion { get; set; }
|
|
private bool StateRefreshInProgress { get; set; }
|
|
private bool HasInteractiveRenderStarted { get; set; }
|
|
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
|
|
|
[Parameter]
|
|
public EventCallback<string?> LoggedOut { get; set; }
|
|
|
|
[Parameter]
|
|
public EventCallback AdminRequested { get; set; }
|
|
|
|
private string? SelectedCampaignName => SelectedCampaign?.Name;
|
|
|
|
private CharacterSummary? SelectedCharacter =>
|
|
SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId);
|
|
|
|
private bool IsCurrentUserGm =>
|
|
SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id;
|
|
|
|
private bool IsCurrentUserAdmin =>
|
|
User is not null && User.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase);
|
|
|
|
private bool CanDeleteSelectedCampaign =>
|
|
SelectedCampaign is not null && User is not null && (SelectedCampaign.Gm.Id == User.Id || IsCurrentUserAdmin);
|
|
|
|
private bool IsSelectedCampaignD6 =>
|
|
string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private List<SkillSummary> SelectedCharacterSkills =>
|
|
SelectedCampaign is null || !SelectedCharacterId.HasValue ? [] : SelectedCampaign.Skills.Where(skill => skill.CharacterId == SelectedCharacterId.Value).OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
|
|
private List<SkillGroupSummary> SelectedCharacterSkillGroups =>
|
|
SelectedCampaign is null || !SelectedCharacterId.HasValue ? [] : SelectedCampaign.SkillGroups.Where(group => group.CharacterId == SelectedCharacterId.Value).OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
|
|
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
|
private bool IsManagementScreen => !IsPlayScreen;
|
|
private string CurrentScreenLabel => IsPlayScreen ? "Play" : "Campaign Management";
|
|
|
|
private string ConnectionStateLabel => ConnectionState switch
|
|
{
|
|
"connected" => "Connected",
|
|
"reconnecting" => "Reconnecting",
|
|
_ => "Offline fallback"
|
|
};
|
|
|
|
private string ConnectionStateCssClass => ConnectionState switch
|
|
{
|
|
"connected" => "ok",
|
|
"reconnecting" => "warn",
|
|
_ => "offline"
|
|
};
|
|
|
|
private string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app";
|
|
|
|
private const string ScreenSessionKey = "screen";
|
|
private const string CampaignSessionKey = "campaign";
|
|
private const string MobilePanelSessionKey = "play-panel";
|
|
private const string RollVisibilitySessionKey = "roll-visibility";
|
|
private const int ToastDurationMs = 3200;
|
|
|
|
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
|
|
}
|