< Summary

Information
Class: Chronicis.Client.Pages.SessionDetail
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/SessionDetail.razor
Line coverage
100%
Covered lines: 117
Uncovered lines: 0
Coverable lines: 117
Total lines: 1248
Line coverage: 100%
Branch coverage
100%
Covered branches: 44
Total branches: 44
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/SessionDetail.razor

#LineLine coverage
 1@attribute [Authorize]
 2@implements IDisposable
 3@using System.ComponentModel
 4@using Chronicis.Client.Components.Articles
 5@using Chronicis.Client.Components.Maps
 6@using Chronicis.Client.Components.Shared
 7@using Chronicis.Client.Services
 8@using Chronicis.Client.Abstractions
 9@using ArticleWikiLinkAutocompleteItem = Chronicis.Client.Components.Articles.WikiLinkAutocompleteItem
 10@using Chronicis.Shared.DTOs
 11@using Chronicis.Shared.Enums
 12@using Microsoft.AspNetCore.Components.Web
 13@using Microsoft.AspNetCore.Authorization
 14@using Microsoft.JSInterop
 15@inject SessionDetailViewModel ViewModel
 16@inject IDialogService DialogService
 17@inject IKeyboardShortcutService KeyboardShortcutService
 18@inject IJSRuntime JSRuntime
 19@inject NavigationManager Navigation
 20@inject ISnackbar Snackbar
 21@inject ILogger<SessionDetail> Logger
 22@inject ILinkApiService LinkApiService
 23@inject IExternalLinkApiService ExternalLinkApiService
 24@inject IMapApiService MapApi
 25@inject IWikiLinkService WikiLinkService
 26@inject IWikiLinkCommitService WikiLinkCommitService
 27@inject IAppContextService AppContext
 28@inject IAppNavigator AppNavigator
 29@inject IArticleCacheService ArticleCache
 30@inject IAISummaryApiService SummaryApi
 31@inject IWorldApiService WorldApi
 32@inject IDrawerCoordinator DrawerCoordinator
 33
 5434@if (ViewModel.IsLoading)
 35{
 36    <LoadingSkeleton />
 37}
 5338else if (ViewModel.Session == null)
 39{
 40    <NotFoundAlert EntityType="Session" />
 41}
 42else
 43{
 44    <MudPaper Elevation="2" Class="chronicis-article-card chronicis-fade-in">
 45        <ChroniclsBreadcrumbs Items="ViewModel.Breadcrumbs" Class="mb-2" />
 46
 47        <div class="d-flex align-center mb-3">
 48            <MudIcon Icon="@Icons.Material.Filled.EventNote"
 49                     Size="Size.Large"
 50                     Class="mr-3 generic-container-icon"
 51                     Style="color: var(--chronicis-beige-gold); font-size: 2.5rem;" />
 52            <div class="flex-grow-1">
 53                @if (ViewModel.CanManageSessionDetails)
 54                {
 55                    <MudTextField @bind-Value="ViewModel.EditName"
 56                                  Variant="Variant.Text"
 57                                  Placeholder="Untitled Session"
 58                                  Immediate="true"
 59                                  Underline="false"
 60                                  Class="flex-grow-1"
 61                                  Style="font-size: 2rem; font-family: var(--chronicis-font-heading);"
 62                                  @onkeydown="OnSessionTitleKeyDown" />
 63                    <div class="d-flex align-center flex-wrap">
 64                        <MudText Typo="Typo.body2" Color="Color.Secondary" Class="mr-2">
 65                            Session Date:
 66                        </MudText>
 67                        <MudDatePicker Date="@ViewModel.EditSessionDate"
 68                                       DateChanged="OnSessionDateChanged"
 69                                       DateFormat="MMMM d, yyyy"
 70                                       Variant="Variant.Text"
 71                                       Margin="Margin.Dense"
 72                                       Underline="false"
 73                                       Clearable="true"
 74                                       Editable="true"
 75                                       Class="mt-0" />
 76                    </div>
 77                }
 78                else
 79                {
 80                    <MudText Typo="Typo.h4" Style="font-family: var(--chronicis-font-heading);">
 81                        @ViewModel.Session.Name
 82                    </MudText>
 83                    <MudText Typo="Typo.body2" Color="Color.Secondary">
 84                        Session Date: @FormatSessionDate(ViewModel.Session.SessionDate)
 85                    </MudText>
 86                }
 87            </div>
 88            @if (ViewModel.CanManageSessionDetails)
 89            {
 90                <MudChip T="string" Color="Color.Warning" Variant="Variant.Outlined" Size="Size.Small">
 91                    GM
 92                </MudChip>
 93            }
 94        </div>
 95
 96        <div class="chronicis-rune-divider mb-4"></div>
 97
 98        <MudTabs Elevation="0"
 99                 Rounded="true"
 100                 ApplyEffectsToContainer="true"
 101                 KeepPanelsAlive="true"
 102                 PanelClass="pa-4 mb-2">
 103            <MudTabPanel Text="Notes" Icon="@Icons.Material.Filled.Public">
 104                <MudText Typo="Typo.h6" Class="mb-2" Style="color: var(--chronicis-beige-gold);">
 105                    <MudIcon Icon="@Icons.Material.Filled.Public" Size="Size.Small" Class="mr-2" />
 106                    Public Notes
 107                </MudText>
 108
 109                @if (ViewModel.CanManageSessionDetails)
 110                {
 111                    <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-1">
 112                        Public Notes
 113                    </MudText>
 114                    <div id="@PublicEditorElementId" class="chronicis-editor-container mb-4"></div>
 115                }
 116                else if (!string.IsNullOrWhiteSpace(ViewModel.Session.PublicNotes))
 117                {
 118                    <MudPaper Outlined="true" Class="pa-4 mb-4">
 119                        @((MarkupString)ViewModel.Session.PublicNotes)
 120                    </MudPaper>
 121                }
 122                else
 123                {
 124                    <MudAlert Severity="Severity.Info" Class="mb-4">
 125                        No public notes yet.
 126                    </MudAlert>
 127                }
 128            </MudTabPanel>
 129
 130            @if (ViewModel.CanViewPrivateNotes)
 131            {
 132                <MudTabPanel Text="Private Notes" Icon="@Icons.Material.Filled.Lock">
 133                    <MudText Typo="Typo.h6" Class="mb-2" Style="color: var(--chronicis-beige-gold);">
 134                        <MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="mr-2" />
 135                        Private Notes
 136                    </MudText>
 137
 138                    @if (ViewModel.CanManageSessionDetails)
 139                    {
 140                        <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-1">
 141                            Private Notes (World owner / GM only)
 142                        </MudText>
 143                        <div id="@PrivateEditorElementId" class="chronicis-editor-container mb-4"></div>
 144                    }
 145                    else
 146                    {
 147                        <MudAlert Severity="Severity.Info" Class="mb-4">
 148                            Private notes are visible to the world owner and GMs only.
 149                        </MudAlert>
 150                    }
 151                </MudTabPanel>
 152            }
 153        </MudTabs>
 154
 155        @if (ViewModel.CanManageSessionDetails)
 156        {
 157            <div class="chronicis-flex-between mb-4">
 158                <SaveStatusIndicator IsSaving="ViewModel.IsSavingNotes" HasUnsavedChanges="ViewModel.HasUnsavedChanges" 
 159
 160                <div class="d-flex align-center">
 161                    <MudButton Variant="Variant.Filled"
 162                               Color="Color.Primary"
 163                               OnClick="SaveSessionNotesAsync"
 164                               Disabled="@(ViewModel.IsSavingNotes || ViewModel.IsDeleting || !ViewModel.HasUnsavedChang
 165                               StartIcon="@Icons.Material.Filled.Save">
 166                        Save
 167                    </MudButton>
 168
 169                    <MudButton Variant="Variant.Outlined"
 170                               Color="Color.Error"
 171                               OnClick="OpenDeleteDialogAsync"
 172                               Disabled="@ViewModel.IsDeleting"
 173                               StartIcon="@Icons.Material.Filled.Delete"
 174                               Class="ml-2">
 175                        @(ViewModel.IsDeleting ? "Deleting..." : "Delete")
 176                    </MudButton>
 177                </div>
 178            </div>
 179        }
 180
 181        <MudDivider Class="my-4" />
 182
 183        <div class="d-flex align-center justify-space-between mb-2">
 184            <MudText Typo="Typo.h6" Style="color: var(--chronicis-beige-gold);">
 185                <MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Small" Class="mr-2" />
 186                AI Summary
 187            </MudText>
 188            <div class="d-flex align-center">
 189                @if (!string.IsNullOrWhiteSpace(ViewModel.Session.AiSummary))
 190                {
 191                    <MudTooltip Text="Refresh summary">
 192                        <MudIconButton Icon="@Icons.Material.Filled.Refresh"
 193                                       Color="Color.Secondary"
 194                                       Disabled="@(ViewModel.IsGeneratingSummary || ViewModel.IsDeletingSummary || ViewM
 195                                       OnClick="@ViewModel.GenerateAiSummaryAsync" />
 196                    </MudTooltip>
 197                    <MudTooltip Text="Delete summary">
 198                        <MudIconButton Icon="@Icons.Material.Filled.Delete"
 199                                       Color="Color.Error"
 200                                       Disabled="@(ViewModel.IsGeneratingSummary || ViewModel.IsDeletingSummary || ViewM
 201                                       OnClick="@ViewModel.ClearAiSummaryAsync" />
 202                    </MudTooltip>
 203                }
 204                else
 205                {
 206                    <MudButton Variant="Variant.Filled"
 207                               Color="Color.Secondary"
 208                               OnClick="@ViewModel.GenerateAiSummaryAsync"
 209                               Disabled="@(ViewModel.IsGeneratingSummary || ViewModel.IsDeleting)"
 210                               StartIcon="@Icons.Material.Filled.AutoAwesome">
 211                        @(ViewModel.IsGeneratingSummary ? "Generating..." : "Generate")
 212                    </MudButton>
 213                }
 214            </div>
 215        </div>
 216
 217        @if (ViewModel.IsGeneratingSummary || ViewModel.IsDeletingSummary)
 218        {
 219            <MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-3" />
 220        }
 221
 222        @if (!string.IsNullOrWhiteSpace(ViewModel.Session.AiSummary))
 223        {
 224            <MudPaper Outlined="true" Class="pa-4 mb-2">
 225                <MudText Typo="Typo.body1" Style="white-space: pre-wrap;">@ViewModel.Session.AiSummary</MudText>
 226            </MudPaper>
 227            @if (ViewModel.Session.AiSummaryGeneratedAt.HasValue)
 228            {
 229                <MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-4">
 230                    Generated @ViewModel.Session.AiSummaryGeneratedAt.Value.ToLocalTime().ToString("g")
 231                </MudText>
 232            }
 233        }
 234        else
 235        {
 236            <MudAlert Severity="Severity.Info" Class="mb-4">
 237                No AI summary yet.
 238            </MudAlert>
 239        }
 240
 241        <MudDivider Class="my-4" />
 242
 243        <div class="d-flex align-center justify-space-between flex-wrap mb-2">
 244            <MudText Typo="Typo.h6" Style="color: var(--chronicis-beige-gold);">
 245                <MudIcon Icon="@Icons.Material.Filled.Description" Size="Size.Small" Class="mr-2" />
 246                Session Notes
 247            </MudText>
 248
 249            @if (ViewModel.CanCreateSessionNote)
 250            {
 251                <MudButton Variant="Variant.Outlined"
 252                           Color="Color.Primary"
 253                           Size="Size.Small"
 254                           OnClick="@ViewModel.CreateSessionNoteAsync"
 255                           Disabled="@(ViewModel.IsCreatingSessionNote || ViewModel.IsDeleting)"
 256                           StartIcon="@Icons.Material.Filled.NoteAdd">
 257                    @(ViewModel.IsCreatingSessionNote ? "Adding..." : "Add Session Note")
 258                </MudButton>
 259            }
 260        </div>
 261
 262        @if (ViewModel.SessionNotes.Any())
 263        {
 264            <MudList T="ArticleTreeDto" Dense="true">
 265                @foreach (var note in ViewModel.SessionNotes)
 266                {
 267                    <EntityListItem Icon="@Icons.Material.Filled.Description"
 268                                    Title="@(string.IsNullOrWhiteSpace(note.Title) ? "Untitled Session Note" : note.Titl
 269                                    OnClick="@(() => ViewModel.OpenSessionNoteAsync(note))">
 270                        <MudChip T="string"
 271                                 Size="Size.Small"
 272                                 Variant="Variant.Outlined"
 273                                 Color="@GetVisibilityColor(note.Visibility)">
 274                            @note.Visibility
 275                        </MudChip>
 276                    </EntityListItem>
 277                }
 278            </MudList>
 279        }
 280        else
 281        {
 282            <MudAlert Severity="Severity.Info">
 283                No attached SessionNote articles found.
 284            </MudAlert>
 285        }
 286
 287        @if (ViewModel.CanManageSessionDetails)
 288        {
 289            <MudDrawer @bind-Open="_externalPreviewOpen"
 290                       Anchor="Anchor.End"
 291                       Variant="@DrawerVariant.Temporary"
 292                       Elevation="9999"
 293                       Class="external-link-preview-drawer">
 294                <MudDrawerHeader>
 295                    <MudSpacer />
 296                    <MudIconButton Icon="@Icons.Material.Filled.Close" OnClick="CloseExternalPreview" />
 297                </MudDrawerHeader>
 298                <MudDrawerContainer>
 299                    <div class="external-link-preview-header">
 300                        @if (!string.IsNullOrWhiteSpace(_externalPreviewSource))
 301                        {
 302                            <span class="external-link-preview-source">@_externalPreviewSource.ToUpperInvariant()</span>
 303                        }
 304                        <MudText Typo="Typo.h6">@(_externalPreviewTitle ?? "External Link")</MudText>
 305                    </div>
 306                    @if (!string.IsNullOrWhiteSpace(_externalPreviewContent?.Kind))
 307                    {
 308                        <MudText Typo="Typo.caption" Style="color: var(--mud-palette-text-secondary);">
 309                            @_externalPreviewContent.Kind
 310                        </MudText>
 311                    }
 312                    @if (_externalPreviewLoading)
 313                    {
 314                        <div class="d-flex justify-center align-center" style="padding: 24px;">
 315                            <MudProgressCircular Indeterminate="true" />
 316                        </div>
 317                    }
 318                    else if (!string.IsNullOrWhiteSpace(_externalPreviewError))
 319                    {
 320                        <MudAlert Severity="Severity.Error">@_externalPreviewError</MudAlert>
 321                    }
 322                    else if (_externalPreviewContent != null)
 323                    {
 324                        <ExternalLinkDetailPanel Content="_externalPreviewContent" />
 325                    }
 326                </MudDrawerContainer>
 327            </MudDrawer>
 328
 329            @if (_isMapModalOpen)
 330            {
 331                <SessionMapViewerModal IsOpen="_isMapModalOpen"
 332                                       WorldId="CurrentWorldId"
 333                                       MapId="_selectedMapId"
 334                                       MapName="@_selectedMapName"
 335                                       OnClose="CloseMapModalAsync" />
 336            }
 337
 338            @if (_showAutocomplete)
 339            {
 340                <ArticleDetailWikiLinkAutocomplete Suggestions="@_autocompleteSuggestions"
 341                                                   Loading="@_autocompleteLoading"
 342                                                   SelectedIndex="@_autocompleteSelectedIndex"
 343                                                   SelectedIndexChanged="OnAutocompleteIndexChanged"
 344                                                   OnSelect="OnAutocompleteSelect"
 345                                                   OnCreate="OnAutocompleteCreate"
 346                                                   Position="@_autocompletePosition"
 347                                                   Query="@_autocompleteQuery"
 348                                                   IsExternalQuery="@_autocompleteIsExternalQuery" />
 349            }
 350        }
 351    </MudPaper>
 352}
 353
 354@code {
 1355    private static readonly DialogOptions _deleteDialogOptions = new()
 1356    {
 1357        CloseOnEscapeKey = true,
 1358        MaxWidth = MaxWidth.Small,
 1359        FullWidth = true,
 1360    };
 361
 362    [Parameter]
 363    public Guid SessionId { get; set; }
 364
 365    private enum SessionEditorKind
 366    {
 367        Public,
 368        Private
 369    }
 370
 371    private sealed class SessionEditorInteropBridge
 372    {
 373        private readonly SessionDetail _parent;
 374        private readonly SessionEditorKind _editorKind;
 375
 376        public SessionEditorInteropBridge(SessionDetail parent, SessionEditorKind editorKind)
 377        {
 32378            _parent = parent;
 32379            _editorKind = editorKind;
 32380        }
 381
 382        [JSInvokable]
 1383        public Task OnEditorUpdate(string html) => _parent.HandleEditorUpdateAsync(_editorKind, html);
 384
 385        [JSInvokable]
 386        public Task OnAutocompleteTriggered(string query, double x, double y)
 1387            => _parent.HandleAutocompleteTriggeredAsync(_editorKind, query, x, y);
 388
 389        [JSInvokable]
 1390        public Task OnAutocompleteHidden() => _parent.HandleAutocompleteHiddenAsync(_editorKind);
 391
 392        [JSInvokable]
 1393        public Task OnAutocompleteArrowDown() => _parent.HandleAutocompleteArrowDownAsync(_editorKind);
 394
 395        [JSInvokable]
 1396        public Task OnAutocompleteArrowUp() => _parent.HandleAutocompleteArrowUpAsync(_editorKind);
 397
 398        [JSInvokable]
 1399        public Task OnAutocompleteEnter() => _parent.HandleAutocompleteEnterAsync(_editorKind);
 400
 401        [JSInvokable]
 2402        public Task OnWikiLinkClicked(string targetArticleId) => _parent.OnWikiLinkClicked(targetArticleId);
 403
 404        [JSInvokable]
 1405        public Task OnBrokenLinkClicked(string targetArticleId) => _parent.OnBrokenLinkClicked(targetArticleId);
 406
 407        [JSInvokable]
 408        public Task OnExternalLinkClicked(string source, string id, string title)
 1409            => _parent.OnExternalLinkClicked(source, id, title);
 410
 411        [JSInvokable]
 412        public Task OnMapLinkClicked(string mapId, string? mapName)
 4413            => _parent.HandleMapLinkClickedAsync(mapId, mapName);
 414
 415        [JSInvokable]
 1416        public Task<string?> GetArticlePath(string targetArticleId) => _parent.GetArticlePath(targetArticleId);
 417
 418        [JSInvokable]
 419        public Task<object?> GetArticleSummaryPreview(string targetArticleId)
 1420            => _parent.GetArticleSummaryPreview(targetArticleId);
 421    }
 422
 423    private SessionEditorInteropBridge? _publicEditorBridge;
 424    private SessionEditorInteropBridge? _privateEditorBridge;
 425    private DotNetObjectReference<SessionEditorInteropBridge>? _publicEditorBridgeRef;
 426    private DotNetObjectReference<SessionEditorInteropBridge>? _privateEditorBridgeRef;
 427    private bool _publicEditorInitialized;
 428    private bool _privateEditorInitialized;
 429    private bool _editorsInitializing;
 430    private SessionEditorKind _activeEditorKind = SessionEditorKind.Public;
 431
 432    private bool _showAutocomplete;
 433    private bool _autocompleteLoading;
 16434    private List<ArticleWikiLinkAutocompleteItem> _autocompleteSuggestions = new();
 435    private int _autocompleteSelectedIndex;
 16436    private (double X, double Y) _autocompletePosition = (0, 0);
 16437    private string _autocompleteQuery = string.Empty;
 438    private bool _autocompleteIsExternalQuery;
 439    private string? _autocompleteExternalSourceKey;
 440    private bool _autocompleteIsMapQuery;
 441
 442    private bool _externalPreviewOpen;
 443    private bool _externalPreviewLoading;
 444    private string? _externalPreviewError;
 445    private ExternalLinkContentDto? _externalPreviewContent;
 446    private string? _externalPreviewSource;
 447    private string? _externalPreviewTitle;
 16448    private readonly Dictionary<string, ExternalLinkContentDto> _externalLinkCache = new(StringComparer.OrdinalIgnoreCas
 449    private bool _isMapModalOpen;
 450    private Guid _selectedMapId;
 451    private string? _selectedMapName;
 452
 131453    private string PublicEditorElementId => $"session-public-editor-{SessionId}";
 130454    private string PrivateEditorElementId => $"session-private-editor-{SessionId}";
 6455    private Guid CurrentWorldId => ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 456
 457    protected override void OnInitialized()
 458    {
 16459        KeyboardShortcutService.OnSaveRequested += HandleSaveShortcut;
 16460        DrawerCoordinator.OnChanged += HandleDrawerCoordinatorChanged;
 16461        _publicEditorBridge = new SessionEditorInteropBridge(this, SessionEditorKind.Public);
 16462        _privateEditorBridge = new SessionEditorInteropBridge(this, SessionEditorKind.Private);
 16463        _publicEditorBridgeRef = DotNetObjectReference.Create(_publicEditorBridge);
 16464        _privateEditorBridgeRef = DotNetObjectReference.Create(_privateEditorBridge);
 16465    }
 466
 467    protected override async Task OnParametersSetAsync()
 468    {
 469        await DestroyEditorsAsync();
 470        ViewModel.PropertyChanged -= OnViewModelChanged;
 471        ViewModel.PropertyChanged += OnViewModelChanged;
 472        await ViewModel.LoadAsync(SessionId);
 473
 474        if (ViewModel.World != null &&
 475            (AppContext.CurrentWorldId != ViewModel.World.Id || AppContext.CurrentCampaignId != ViewModel.Campaign?.Id))
 476        {
 477            await AppContext.SelectWorldAsync(ViewModel.World.Id, ViewModel.Campaign?.Id);
 478        }
 479    }
 480
 481    protected override async Task OnAfterRenderAsync(bool firstRender)
 482    {
 483        if (ViewModel.IsLoading || !ViewModel.CanManageSessionDetails || ViewModel.Session == null || ViewModel.Session.
 484        {
 485            return;
 486        }
 487
 488        if (_editorsInitializing || (_publicEditorInitialized && _privateEditorInitialized))
 489        {
 490            return;
 491        }
 492
 493        await InitializeEditorsAsync();
 494    }
 495
 496    private void OnViewModelChanged(object? sender, PropertyChangedEventArgs e)
 180497        => InvokeAsync(StateHasChanged);
 498
 499    private async Task OnSessionTitleKeyDown(KeyboardEventArgs e)
 500    {
 501        if (e.Key == "Enter" && ViewModel.CanManageSessionDetails)
 502        {
 503            await SaveSessionNotesAsync();
 504        }
 505    }
 506
 507    private async void HandleSaveShortcut()
 508    {
 509        if (!ViewModel.CanManageSessionDetails || ViewModel.Session == null)
 510        {
 511            return;
 512        }
 513
 514        await InvokeAsync(SaveSessionNotesAsync);
 515    }
 516
 517    private void OnSessionDateChanged(DateTime? value)
 518    {
 3519        ViewModel.EditSessionDate = value?.Date;
 3520    }
 521
 522    private async Task OpenDeleteDialogAsync()
 523    {
 524        if (!ViewModel.IsCurrentUserGM || ViewModel.Session == null || ViewModel.IsDeleting)
 525        {
 526            return;
 527        }
 528
 529        var parameters = new DialogParameters<DeleteSessionDialog>
 530        {
 531            { x => x.SessionName, ViewModel.Session.Name }
 532        };
 533
 534        var dialog = await DialogService.ShowAsync<DeleteSessionDialog>(
 535            $"Delete \"{ViewModel.Session.Name}\"", parameters, _deleteDialogOptions);
 536
 537        var result = await dialog.Result;
 538        if (result is { Canceled: false })
 539        {
 540            await ViewModel.DeleteSessionAsync();
 541        }
 542    }
 543
 544    private static string FormatSessionDate(DateTime? sessionDate)
 4545        => sessionDate?.ToLocalTime().ToString("MMMM d, yyyy") ?? "Not set";
 546
 4547    private static Color GetVisibilityColor(ArticleVisibility visibility) => visibility switch
 4548    {
 1549        ArticleVisibility.Public => Color.Success,
 1550        ArticleVisibility.MembersOnly => Color.Warning,
 1551        ArticleVisibility.Private => Color.Error,
 1552        _ => Color.Default
 4553    };
 554
 555    public void Dispose()
 556    {
 16557        KeyboardShortcutService.OnSaveRequested -= HandleSaveShortcut;
 16558        DrawerCoordinator.OnChanged -= HandleDrawerCoordinatorChanged;
 16559        ViewModel.PropertyChanged -= OnViewModelChanged;
 16560        _publicEditorBridgeRef?.Dispose();
 16561        _privateEditorBridgeRef?.Dispose();
 15562    }
 563
 564    private void HandleDrawerCoordinatorChanged()
 565    {
 3566        if (!_externalPreviewOpen || DrawerCoordinator.Current == DrawerType.None)
 567        {
 2568            return;
 569        }
 570
 1571        _externalPreviewOpen = false;
 1572        _ = InvokeAsync(StateHasChanged);
 1573    }
 574
 575    private async Task InitializeEditorsAsync()
 576    {
 577        if (ViewModel.Session == null || !ViewModel.CanManageSessionDetails || _publicEditorBridgeRef == null || _privat
 578        {
 579            return;
 580        }
 581
 582        _editorsInitializing = true;
 583
 584        try
 585        {
 586            await JSRuntime.InvokeVoidAsync("initializeTipTapEditor", PublicEditorElementId, ViewModel.EditPublicNotes, 
 587            await JSRuntime.InvokeVoidAsync("initializeWikiLinkAutocomplete", PublicEditorElementId, _publicEditorBridge
 588            await JSRuntime.InvokeVoidAsync("initializeImageUpload", PublicEditorElementId, _publicEditorBridgeRef);
 589            await JSRuntime.InvokeVoidAsync("resolveEditorImages", PublicEditorElementId, _publicEditorBridgeRef);
 590            _publicEditorInitialized = true;
 591
 592            await JSRuntime.InvokeVoidAsync("initializeTipTapEditor", PrivateEditorElementId, ViewModel.EditPrivateNotes
 593            await JSRuntime.InvokeVoidAsync("initializeWikiLinkAutocomplete", PrivateEditorElementId, _privateEditorBrid
 594            await JSRuntime.InvokeVoidAsync("initializeImageUpload", PrivateEditorElementId, _privateEditorBridgeRef);
 595            await JSRuntime.InvokeVoidAsync("resolveEditorImages", PrivateEditorElementId, _privateEditorBridgeRef);
 596            _privateEditorInitialized = true;
 597        }
 598        catch (JSDisconnectedException)
 599        {
 600            // Navigation/disposal race.
 601        }
 602        catch (Exception ex)
 603        {
 604            Logger.LogError(ex, "Failed to initialize session note editors for {SessionId}", SessionId);
 605            Snackbar.Add("Failed to initialize session editors", Severity.Warning);
 606        }
 607        finally
 608        {
 609            _editorsInitializing = false;
 610        }
 611    }
 612
 613    private async Task DestroyEditorsAsync()
 614    {
 615        try
 616        {
 617            if (_publicEditorInitialized)
 618            {
 619                await JSRuntime.InvokeVoidAsync("destroyTipTapEditor", PublicEditorElementId);
 620            }
 621
 622            if (_privateEditorInitialized)
 623            {
 624                await JSRuntime.InvokeVoidAsync("destroyTipTapEditor", PrivateEditorElementId);
 625            }
 626        }
 627        catch (JSDisconnectedException)
 628        {
 629            // Ignore during navigation/disposal.
 630        }
 631        catch (Exception ex)
 632        {
 633            Logger.LogDebug(ex, "Failed to destroy session editors");
 634        }
 635        finally
 636        {
 637            _publicEditorInitialized = false;
 638            _privateEditorInitialized = false;
 639            _showAutocomplete = false;
 640            _autocompleteSuggestions = new();
 641        }
 642    }
 643
 644    private async Task SaveSessionNotesAsync()
 645    {
 646        await SyncEditorsToViewModelAsync();
 647        await ViewModel.SaveNotesAsync();
 648    }
 649
 650    private async Task SyncEditorsToViewModelAsync()
 651    {
 652        if (!ViewModel.CanManageSessionDetails)
 653        {
 654            return;
 655        }
 656
 657        try
 658        {
 659            if (_publicEditorInitialized)
 660            {
 661                ViewModel.EditPublicNotes = await JSRuntime.InvokeAsync<string>("getTipTapContent", PublicEditorElementI
 662            }
 663
 664            if (_privateEditorInitialized)
 665            {
 666                ViewModel.EditPrivateNotes = await JSRuntime.InvokeAsync<string>("getTipTapContent", PrivateEditorElemen
 667            }
 668        }
 669        catch (JSDisconnectedException)
 670        {
 671            // Ignore during navigation/disposal.
 672        }
 673    }
 674
 675    private Task HandleEditorUpdateAsync(SessionEditorKind editorKind, string html)
 676    {
 4677        if (!ViewModel.CanManageSessionDetails)
 678        {
 1679            return Task.CompletedTask;
 680        }
 681
 3682        if (editorKind == SessionEditorKind.Public)
 683        {
 2684            ViewModel.EditPublicNotes = html;
 685        }
 686        else
 687        {
 1688            ViewModel.EditPrivateNotes = html;
 689        }
 690
 3691        return Task.CompletedTask;
 692    }
 693
 694    private async Task HandleAutocompleteTriggeredAsync(SessionEditorKind editorKind, string query, double x, double y)
 695    {
 696        var normalizedQuery = query?.TrimStart() ?? string.Empty;
 697        _activeEditorKind = editorKind;
 698        _autocompletePosition = (x, y);
 699        _showAutocomplete = true;
 700        _autocompleteSelectedIndex = 0;
 701        var isMapAutocomplete = normalizedQuery.StartsWith("maps/", StringComparison.OrdinalIgnoreCase);
 702        string? mapSearch = null;
 703
 704        if (isMapAutocomplete)
 705        {
 706            var remainder = normalizedQuery.Substring("maps/".Length);
 707            mapSearch = string.IsNullOrWhiteSpace(remainder) ? null : remainder.Trim();
 708            _autocompleteIsExternalQuery = false;
 709            _autocompleteExternalSourceKey = null;
 710            _autocompleteQuery = mapSearch ?? string.Empty;
 711        }
 712        else
 713        {
 714            _autocompleteIsExternalQuery = TryParseExternalAutocompleteQuery(normalizedQuery, out var sourceKey, out var
 715            var mapsExternalSource = _autocompleteIsExternalQuery
 716                && string.Equals(sourceKey, "maps", StringComparison.OrdinalIgnoreCase);
 717
 718            if (mapsExternalSource)
 719            {
 720                isMapAutocomplete = true;
 721                mapSearch = string.IsNullOrWhiteSpace(remainder) ? null : remainder.Trim();
 722                _autocompleteIsExternalQuery = false;
 723                _autocompleteExternalSourceKey = null;
 724                _autocompleteQuery = mapSearch ?? string.Empty;
 725            }
 726            else
 727            {
 728                _autocompleteExternalSourceKey = _autocompleteIsExternalQuery ? sourceKey : null;
 729                _autocompleteQuery = _autocompleteIsExternalQuery ? remainder : normalizedQuery;
 730            }
 731
 732            var minLength = _autocompleteIsExternalQuery ? 0 : 3;
 733            if (!isMapAutocomplete && _autocompleteQuery.Length < minLength)
 734            {
 735                _autocompleteIsMapQuery = false;
 736                _autocompleteSuggestions = new();
 737                await InvokeAsync(StateHasChanged);
 738                return;
 739            }
 740        }
 741
 742        _autocompleteIsMapQuery = isMapAutocomplete;
 743        _autocompleteLoading = true;
 744        await InvokeAsync(StateHasChanged);
 745
 746        try
 747        {
 748            var worldId = ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 749
 750            if (isMapAutocomplete)
 751            {
 752                var mapSuggestions = await MapApi.GetMapAutocompleteAsync(worldId, mapSearch);
 753                _autocompleteSuggestions = mapSuggestions
 754                    .Select(ArticleWikiLinkAutocompleteItem.FromMapAutocomplete)
 755                    .ToList();
 756            }
 757            else if (_autocompleteIsExternalQuery)
 758            {
 759                var externalSuggestions = await ExternalLinkApiService.GetSuggestionsAsync(
 760                    worldId,
 761                    _autocompleteExternalSourceKey ?? string.Empty,
 762                    _autocompleteQuery,
 763                    CancellationToken.None);
 764
 765                _autocompleteSuggestions = externalSuggestions.Select(ArticleWikiLinkAutocompleteItem.FromExternal).ToLi
 766            }
 767            else
 768            {
 769                var internalSuggestions = await LinkApiService.GetSuggestionsAsync(worldId, _autocompleteQuery);
 770                _autocompleteSuggestions = internalSuggestions.Select(ArticleWikiLinkAutocompleteItem.FromInternal).ToLi
 771            }
 772        }
 773        catch (Exception ex)
 774        {
 775            Logger.LogError(ex, "Error getting session editor autocomplete suggestions");
 776            _autocompleteSuggestions = new();
 777        }
 778        finally
 779        {
 780            _autocompleteLoading = false;
 781            await InvokeAsync(StateHasChanged);
 782        }
 783    }
 784
 785    private Task HandleAutocompleteHiddenAsync(SessionEditorKind editorKind)
 786    {
 3787        if (_activeEditorKind != editorKind)
 788        {
 1789            return Task.CompletedTask;
 790        }
 791
 2792        _showAutocomplete = false;
 2793        _autocompleteSuggestions = new();
 2794        _autocompleteIsExternalQuery = false;
 2795        _autocompleteExternalSourceKey = null;
 2796        return InvokeAsync(StateHasChanged);
 797    }
 798
 799    private Task HandleAutocompleteArrowDownAsync(SessionEditorKind editorKind)
 800    {
 3801        _activeEditorKind = editorKind;
 802
 3803        if (_autocompleteSuggestions.Any())
 804        {
 1805            _autocompleteSelectedIndex = (_autocompleteSelectedIndex + 1) % _autocompleteSuggestions.Count;
 1806            return InvokeAsync(StateHasChanged);
 807        }
 808
 2809        return Task.CompletedTask;
 810    }
 811
 812    private Task HandleAutocompleteArrowUpAsync(SessionEditorKind editorKind)
 813    {
 3814        _activeEditorKind = editorKind;
 815
 3816        if (_autocompleteSuggestions.Any())
 817        {
 1818            _autocompleteSelectedIndex = (_autocompleteSelectedIndex - 1 + _autocompleteSuggestions.Count) % _autocomple
 1819            return InvokeAsync(StateHasChanged);
 820        }
 821
 2822        return Task.CompletedTask;
 823    }
 824
 825    private async Task HandleAutocompleteEnterAsync(SessionEditorKind editorKind)
 826    {
 827        _activeEditorKind = editorKind;
 828
 829        var decision = WikiLinkCommitService.Decide(
 830            _autocompleteQuery,
 831            _autocompleteSuggestions.Count,
 832            _autocompleteSelectedIndex,
 833            _autocompleteIsExternalQuery,
 834            _autocompleteIsMapQuery);
 835
 836        switch (decision)
 837        {
 838            case AutocompleteCommitDecision.SelectExisting select:
 839                await OnAutocompleteSelect(_autocompleteSuggestions[select.Index]);
 840                break;
 841            case AutocompleteCommitDecision.CreateNew create:
 842                await OnAutocompleteCreate(create.Name);
 843                break;
 844        }
 845    }
 846
 847    private Task OnAutocompleteIndexChanged(int index)
 848    {
 1849        _autocompleteSelectedIndex = index;
 1850        StateHasChanged();
 1851        return Task.CompletedTask;
 852    }
 853
 854    private async Task OnAutocompleteSelect(ArticleWikiLinkAutocompleteItem suggestion)
 855    {
 856        if (!ViewModel.CanManageSessionDetails)
 857        {
 858            return;
 859        }
 860
 861        var editorElementId = _activeEditorKind == SessionEditorKind.Public ? PublicEditorElementId : PrivateEditorEleme
 862
 863        try
 864        {
 865            if (suggestion.IsCategory && !string.IsNullOrEmpty(suggestion.CategoryKey))
 866            {
 867                await JSRuntime.InvokeVoidAsync("updateAutocompleteText", editorElementId, $"{suggestion.Source}/{sugges
 868                return;
 869            }
 870
 871            if (suggestion.MapId.HasValue)
 872            {
 873                await JSRuntime.InvokeVoidAsync(
 874                    "insertMapLinkToken",
 875                    editorElementId,
 876                    suggestion.MapId.Value.ToString(),
 877                    suggestion.Title);
 878            }
 879
 880            else if (suggestion.IsExternal)
 881            {
 882                if (string.IsNullOrWhiteSpace(suggestion.Source) || string.IsNullOrWhiteSpace(suggestion.ExternalId))
 883                {
 884                    Logger.LogWarning("External suggestion missing source or id");
 885                    return;
 886                }
 887
 888                await JSRuntime.InvokeVoidAsync(
 889                    "insertExternalLinkToken",
 890                    editorElementId,
 891                    suggestion.Source,
 892                    suggestion.ExternalId,
 893                    suggestion.Title);
 894            }
 895            else
 896            {
 897                if (!suggestion.ArticleId.HasValue)
 898                {
 899                    Logger.LogWarning("Internal suggestion missing article id");
 900                    return;
 901                }
 902
 903                var displayText = !string.IsNullOrWhiteSpace(suggestion.MatchedAlias)
 904                    ? $"{suggestion.MatchedAlias} â†’ {suggestion.Title}"
 905                    : suggestion.Title;
 906
 907                await JSRuntime.InvokeVoidAsync("insertWikiLink", editorElementId, suggestion.ArticleId.Value.ToString()
 908            }
 909
 910            _showAutocomplete = false;
 911            _autocompleteSuggestions = new();
 912            StateHasChanged();
 913        }
 914        catch (Exception ex)
 915        {
 916            Logger.LogError(ex, "Error inserting session editor link");
 917            Snackbar.Add("Failed to insert link", Severity.Error);
 918        }
 919    }
 920
 921    private async Task OnAutocompleteCreate(string articleName)
 922    {
 923        if (_autocompleteIsExternalQuery || string.IsNullOrWhiteSpace(articleName))
 924        {
 925            return;
 926        }
 927
 928        var worldId = AppContext.CurrentWorldId ?? ViewModel.World?.Id ?? Guid.Empty;
 929        if (worldId == Guid.Empty)
 930        {
 931            Snackbar.Add("Unable to determine world for new article", Severity.Warning);
 932            return;
 933        }
 934
 935        var editorElementId = _activeEditorKind == SessionEditorKind.Public ? PublicEditorElementId : PrivateEditorEleme
 936
 937        try
 938        {
 939            var created = await WikiLinkService.CreateArticleFromAutocompleteAsync(articleName, worldId);
 940            if (created == null)
 941            {
 942                Snackbar.Add("Failed to create article", Severity.Error);
 943                return;
 944            }
 945
 946            await JSRuntime.InvokeVoidAsync("insertWikiLink", editorElementId, created.Id.ToString(), created.Title);
 947            _showAutocomplete = false;
 948            _autocompleteSuggestions = new();
 949            StateHasChanged();
 950            Snackbar.Add($"Created and linked '{articleName}'", Severity.Success);
 951        }
 952        catch (Exception ex)
 953        {
 954            Logger.LogError(ex, "Error creating article from session editor autocomplete");
 955            Snackbar.Add($"Failed to create article: {ex.Message}", Severity.Error);
 956        }
 957    }
 958
 959    private static bool TryParseExternalAutocompleteQuery(string query, out string sourceKey, out string remainder)
 960    {
 8961        sourceKey = string.Empty;
 8962        remainder = string.Empty;
 963
 8964        if (string.IsNullOrWhiteSpace(query))
 965        {
 1966            return false;
 967        }
 968
 7969        var slashIndex = query.IndexOf('/');
 7970        if (slashIndex <= 0)
 971        {
 4972            return false;
 973        }
 974
 3975        sourceKey = query.Substring(0, slashIndex).Trim().ToLowerInvariant();
 3976        remainder = query.Substring(slashIndex + 1);
 3977        return !string.IsNullOrWhiteSpace(sourceKey);
 978    }
 979
 980    private Task HandleMapLinkClickedAsync(string mapId, string? mapName)
 981    {
 4982        if (!Guid.TryParse(mapId, out var parsedMapId))
 983        {
 1984            return Task.CompletedTask;
 985        }
 986
 3987        _selectedMapId = parsedMapId;
 3988        _selectedMapName = string.IsNullOrWhiteSpace(mapName) ? null : mapName.Trim();
 3989        _isMapModalOpen = true;
 990
 3991        return InvokeAsync(StateHasChanged);
 992    }
 993
 994    private Task CloseMapModalAsync()
 995    {
 1996        _isMapModalOpen = false;
 1997        _selectedMapId = Guid.Empty;
 1998        _selectedMapName = null;
 999
 11000        return InvokeAsync(StateHasChanged);
 1001    }
 1002
 1003    private async Task OnWikiLinkClicked(string targetArticleId)
 1004    {
 1005        if (!Guid.TryParse(targetArticleId, out var articleId))
 1006        {
 1007            return;
 1008        }
 1009
 1010        try
 1011        {
 1012            var info = await ArticleCache.GetArticleInfoAsync(articleId);
 1013            if (info?.FullArticle != null)
 1014            {
 1015                await AppNavigator.GoToArticleAsync(info.FullArticle);
 1016            }
 1017            else
 1018            {
 1019                Snackbar.Add("Article not found", Severity.Warning);
 1020            }
 1021        }
 1022        catch (Exception ex)
 1023        {
 1024            Logger.LogError(ex, "Error navigating to wiki link {ArticleId}", targetArticleId);
 1025            Snackbar.Add("Failed to navigate to article", Severity.Error);
 1026        }
 1027    }
 1028
 1029    private Task OnBrokenLinkClicked(string targetArticleId)
 1030    {
 21031        Snackbar.Add("This link points to a missing article", Severity.Warning);
 21032        return Task.CompletedTask;
 1033    }
 1034
 1035    private async Task<string?> GetArticlePath(string targetArticleId)
 1036    {
 1037        if (!Guid.TryParse(targetArticleId, out var articleId))
 1038        {
 1039            return null;
 1040        }
 1041
 1042        try
 1043        {
 1044            return await ArticleCache.GetArticlePathAsync(articleId);
 1045        }
 1046        catch (Exception ex)
 1047        {
 1048            Logger.LogError(ex, "Error getting article path {ArticleId}", targetArticleId);
 1049            return null;
 1050        }
 1051    }
 1052
 1053    private async Task<object?> GetArticleSummaryPreview(string targetArticleId)
 1054    {
 1055        if (!Guid.TryParse(targetArticleId, out var articleId))
 1056        {
 1057            return null;
 1058        }
 1059
 1060        try
 1061        {
 1062            var preview = await SummaryApi.GetSummaryPreviewAsync(articleId);
 1063            if (preview == null || !preview.HasSummary)
 1064            {
 1065                return null;
 1066            }
 1067
 1068            return new
 1069            {
 1070                title = preview.Title,
 1071                summary = preview.Summary,
 1072                templateName = preview.TemplateName
 1073            };
 1074        }
 1075        catch (Exception ex)
 1076        {
 1077            Logger.LogError(ex, "Error getting article summary preview {ArticleId}", targetArticleId);
 1078            return null;
 1079        }
 1080    }
 1081
 1082    private async Task OnExternalLinkClicked(string source, string id, string title)
 1083    {
 1084        if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(id))
 1085        {
 1086            return;
 1087        }
 1088
 1089        DrawerCoordinator.Close();
 1090        if (DrawerCoordinator.Current != DrawerType.None)
 1091        {
 1092            return;
 1093        }
 1094
 1095        _externalPreviewOpen = true;
 1096        _externalPreviewLoading = true;
 1097        _externalPreviewError = null;
 1098        _externalPreviewSource = source;
 1099        _externalPreviewTitle = string.IsNullOrWhiteSpace(title) ? "External Link" : title;
 1100        _externalPreviewContent = null;
 1101        StateHasChanged();
 1102
 1103        var cacheKey = $"{source}:{id}".ToLowerInvariant();
 1104        if (_externalLinkCache.TryGetValue(cacheKey, out var cached))
 1105        {
 1106            _externalPreviewContent = cached;
 1107            _externalPreviewLoading = false;
 1108            StateHasChanged();
 1109            return;
 1110        }
 1111
 1112        try
 1113        {
 1114            var content = await ExternalLinkApiService.GetContentAsync(source, id, CancellationToken.None);
 1115
 1116            if (content == null || string.IsNullOrWhiteSpace(content.Markdown))
 1117            {
 1118                _externalPreviewError = "No content available.";
 1119            }
 1120            else
 1121            {
 1122                _externalPreviewContent = content;
 1123                _externalLinkCache[cacheKey] = content;
 1124            }
 1125        }
 1126        catch (Exception ex)
 1127        {
 1128            Logger.LogError(ex, "Error loading external link preview for {Source} {Id}", source, id);
 1129            _externalPreviewError = "Failed to load external content.";
 1130        }
 1131        finally
 1132        {
 1133            _externalPreviewLoading = false;
 1134            StateHasChanged();
 1135        }
 1136    }
 1137
 1138    private void CloseExternalPreview()
 1139    {
 11140        _externalPreviewOpen = false;
 11141        StateHasChanged();
 11142    }
 1143
 1144    private async Task InsertPrivateImageFromToolbarAsync()
 1145    {
 1146        if (!_privateEditorInitialized || _privateEditorBridgeRef == null)
 1147        {
 1148            return;
 1149        }
 1150
 1151        await JSRuntime.InvokeVoidAsync("triggerImageUpload", PrivateEditorElementId, _privateEditorBridgeRef);
 1152    }
 1153
 1154    [JSInvokable]
 1155    public async Task<object?> OnImageUploadRequested(string fileName, string contentType, long fileSize)
 1156    {
 1157        var worldId = ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 1158        if (worldId == Guid.Empty)
 1159        {
 1160            return null;
 1161        }
 1162
 1163        try
 1164        {
 1165            var request = new WorldDocumentUploadRequestDto
 1166            {
 1167                FileName = fileName,
 1168                ContentType = contentType,
 1169                FileSizeBytes = fileSize,
 1170                ArticleId = null,
 1171                Description = $"Inline image for session private notes: {ViewModel.Session?.Name ?? "Session"}"
 1172            };
 1173
 1174            var response = await WorldApi.RequestDocumentUploadAsync(worldId, request);
 1175            if (response == null)
 1176            {
 1177                return null;
 1178            }
 1179
 1180            return new
 1181            {
 1182                uploadUrl = response.UploadUrl,
 1183                documentId = response.DocumentId.ToString()
 1184            };
 1185        }
 1186        catch (Exception ex)
 1187        {
 1188            Logger.LogError(ex, "Error requesting session private note image upload for world {WorldId}", worldId);
 1189            return null;
 1190        }
 1191    }
 1192
 1193    [JSInvokable]
 1194    public async Task OnImageUploadConfirmed(string documentIdStr)
 1195    {
 1196        var worldId = ViewModel.World?.Id ?? AppContext.CurrentWorldId ?? Guid.Empty;
 1197        if (worldId == Guid.Empty || !Guid.TryParse(documentIdStr, out var documentId))
 1198        {
 1199            return;
 1200        }
 1201
 1202        try
 1203        {
 1204            await WorldApi.ConfirmDocumentUploadAsync(worldId, documentId);
 1205        }
 1206        catch (Exception ex)
 1207        {
 1208            Logger.LogError(ex, "Error confirming session private note image upload {DocumentId}", documentId);
 1209        }
 1210    }
 1211
 1212    [JSInvokable]
 11213    public string GetImageProxyUrl(string documentIdStr) => $"chronicis-image:{documentIdStr}";
 1214
 1215    [JSInvokable]
 1216    public async Task<string?> ResolveImageUrl(string documentIdStr)
 1217    {
 1218        if (!Guid.TryParse(documentIdStr, out var documentId))
 1219        {
 1220            return null;
 1221        }
 1222
 1223        try
 1224        {
 1225            var result = await WorldApi.DownloadDocumentAsync(documentId);
 1226            return result?.DownloadUrl;
 1227        }
 1228        catch (Exception ex)
 1229        {
 1230            Logger.LogError(ex, "Error resolving session private note image {DocumentId}", documentId);
 1231            return null;
 1232        }
 1233    }
 1234
 1235    [JSInvokable]
 1236    public void OnImageUploadStarted(string fileName)
 1237    {
 11238        Snackbar.Add($"Uploading {fileName}...", Severity.Info);
 11239        StateHasChanged();
 11240    }
 1241
 1242    [JSInvokable]
 1243    public void OnImageUploadError(string message)
 1244    {
 11245        Snackbar.Add(message, Severity.Error);
 11246        StateHasChanged();
 11247    }
 1248}

Methods/Properties

BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder)
.cctor()
.ctor(Chronicis.Client.Pages.SessionDetail,Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
OnEditorUpdate(System.String)
OnAutocompleteTriggered(System.String,System.Double,System.Double)
OnAutocompleteHidden()
OnAutocompleteArrowDown()
OnAutocompleteArrowUp()
OnAutocompleteEnter()
OnWikiLinkClicked(System.String)
OnBrokenLinkClicked(System.String)
OnExternalLinkClicked(System.String,System.String,System.String)
OnMapLinkClicked(System.String,System.String)
GetArticlePath(System.String)
GetArticleSummaryPreview(System.String)
.ctor()
get_PublicEditorElementId()
get_PrivateEditorElementId()
get_CurrentWorldId()
OnInitialized()
OnViewModelChanged(System.Object,System.ComponentModel.PropertyChangedEventArgs)
OnSessionDateChanged(System.Nullable`1<System.DateTime>)
FormatSessionDate(System.Nullable`1<System.DateTime>)
GetVisibilityColor(Chronicis.Shared.Enums.ArticleVisibility)
Dispose()
HandleDrawerCoordinatorChanged()
HandleEditorUpdateAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind,System.String)
HandleAutocompleteHiddenAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
HandleAutocompleteArrowDownAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
HandleAutocompleteArrowUpAsync(Chronicis.Client.Pages.SessionDetail/SessionEditorKind)
OnAutocompleteIndexChanged(System.Int32)
TryParseExternalAutocompleteQuery(System.String,System.String&,System.String&)
HandleMapLinkClickedAsync(System.String,System.String)
CloseMapModalAsync()
OnBrokenLinkClicked(System.String)
CloseExternalPreview()
GetImageProxyUrl(System.String)
OnImageUploadStarted(System.String)
OnImageUploadError(System.String)