934 lines
31 KiB
Plaintext
934 lines
31 KiB
Plaintext
@page "/print"
|
|
@attribute [Authorize]
|
|
@implements IAsyncDisposable
|
|
@using Core.Printing
|
|
@inject INotesService NotesService
|
|
@inject INotePrintService NotePrintService
|
|
@inject IPrintPresetService PrintPresetService
|
|
@inject IConfiguration Configuration
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
@inject IJSRuntime JSRuntime
|
|
@inject NavigationManager NavigationManager
|
|
@inject MarkdownTablePasteService MarkdownTablePasteService
|
|
|
|
<div class="no-print">
|
|
<PageHeader Title="Page printer"
|
|
Description="Write a markdown template, merge it onto students, teams, or events, and print."
|
|
Icon="@Icons.Material.Filled.Print" />
|
|
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6 mb-4">
|
|
@if (_isLoading)
|
|
{
|
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
|
|
}
|
|
|
|
<MudGrid>
|
|
<MudItem xs="12" md="5">
|
|
<MudSelect T="int?"
|
|
Label="Print preset"
|
|
Value="_selectedPresetId"
|
|
ValueChanged="OnPresetSelected"
|
|
Clearable="true"
|
|
Variant="Variant.Outlined">
|
|
@foreach (var preset in _presets)
|
|
{
|
|
<MudSelectItem T="int?" Value="@preset.Id">@preset.Name</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
</MudItem>
|
|
<MudItem xs="12" md="7" Class="d-flex align-center gap-2 flex-wrap">
|
|
<MudButton Variant="Variant.Outlined"
|
|
OnClick="NewTemplate"
|
|
Disabled="@_isBusy">
|
|
New
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined"
|
|
StartIcon="@Icons.Material.Filled.Save"
|
|
OnClick="SavePreset"
|
|
Disabled="@_isBusy">
|
|
Save
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined"
|
|
Color="Color.Error"
|
|
OnClick="DeletePreset"
|
|
Disabled="@(_isBusy || !_selectedPresetId.HasValue)">
|
|
Delete
|
|
</MudButton>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12" md="4">
|
|
<MudSelect T="PrintEntityType"
|
|
Label="Entity"
|
|
Value="_entityType"
|
|
ValueChanged="OnEntityTypeChanged"
|
|
Variant="Variant.Outlined">
|
|
<MudSelectItem Value="PrintEntityType.Student">Students</MudSelectItem>
|
|
<MudSelectItem Value="PrintEntityType.Team">Teams</MudSelectItem>
|
|
<MudSelectItem Value="PrintEntityType.Event">Events</MudSelectItem>
|
|
</MudSelect>
|
|
</MudItem>
|
|
|
|
@if (_entityType == PrintEntityType.Student)
|
|
{
|
|
<MudItem xs="12" sm="4" md="2">
|
|
<MudNumericField T="int?"
|
|
Label="Grade"
|
|
Value="_grade"
|
|
ValueChanged="OnGradeChanged"
|
|
Variant="Variant.Outlined"
|
|
Min="5"
|
|
Max="12"
|
|
Clearable="true" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4" md="2">
|
|
<MudNumericField T="int?"
|
|
Label="TSA year"
|
|
Value="_tsaYear"
|
|
ValueChanged="OnTsaYearChanged"
|
|
Variant="Variant.Outlined"
|
|
Min="1"
|
|
Max="12"
|
|
Clearable="true" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4" md="4">
|
|
<MudSelect T="string"
|
|
Label="Officer"
|
|
Value="@_officerChoice"
|
|
ValueChanged="OnOfficerChanged"
|
|
Variant="Variant.Outlined">
|
|
<MudSelectItem Value="@("any")">Any</MudSelectItem>
|
|
<MudSelectItem Value="@("yes")">Officers only</MudSelectItem>
|
|
<MudSelectItem Value="@("no")">Non-officers only</MudSelectItem>
|
|
</MudSelect>
|
|
</MudItem>
|
|
}
|
|
else if (_entityType == PrintEntityType.Team)
|
|
{
|
|
<MudItem xs="12" md="8">
|
|
<MudTextField T="string"
|
|
Value="@_teamIdentifierContains"
|
|
ValueChanged="OnTeamIdentifierChanged"
|
|
Label="Identifier contains"
|
|
Variant="Variant.Outlined"
|
|
Immediate="true" />
|
|
</MudItem>
|
|
}
|
|
else
|
|
{
|
|
<MudItem xs="12" sm="4">
|
|
<MudTextField T="string"
|
|
Value="@_eventNameContains"
|
|
ValueChanged="OnEventNameChanged"
|
|
Label="Name contains"
|
|
Variant="Variant.Outlined"
|
|
Immediate="true" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4">
|
|
<MudSelect T="EventFormat?"
|
|
Label="Event format"
|
|
Value="_eventFormat"
|
|
ValueChanged="OnEventFormatChanged"
|
|
Variant="Variant.Outlined"
|
|
Clearable="true">
|
|
@foreach (var format in Enum.GetValues<EventFormat>())
|
|
{
|
|
<MudSelectItem T="EventFormat?" Value="@format">@format</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4">
|
|
<MudSelect T="string"
|
|
Label="Regional"
|
|
Value="@_regionalChoice"
|
|
ValueChanged="OnRegionalChanged"
|
|
Variant="Variant.Outlined">
|
|
<MudSelectItem Value="@("any")">Any</MudSelectItem>
|
|
<MudSelectItem Value="@("yes")">Regional only</MudSelectItem>
|
|
<MudSelectItem Value="@("no")">Non-regional only</MudSelectItem>
|
|
</MudSelect>
|
|
</MudItem>
|
|
}
|
|
|
|
<MudItem xs="12">
|
|
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap mb-2">
|
|
<MudButton Variant="Variant.Outlined"
|
|
StartIcon="@Icons.Material.Filled.DataObject"
|
|
OnClick="OpenTokenDialog"
|
|
Disabled="@_isBusy">
|
|
Insert token
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Visibility"
|
|
OnClick="Preview"
|
|
Disabled="@_isBusy">
|
|
Preview
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined"
|
|
StartIcon="@Icons.Material.Filled.Print"
|
|
OnClick="Print"
|
|
Disabled="@(_isBusy || _previewStale || _pages.Count == 0)">
|
|
Print
|
|
</MudButton>
|
|
<MudCheckBox T="bool"
|
|
Value="@_newPagePerRecord"
|
|
ValueChanged="OnNewPagePerRecordChanged"
|
|
Label="New page per record"
|
|
Dense="true" />
|
|
<MudNumericField T="int"
|
|
Label="Font size (pt)"
|
|
Value="_fontSizePt"
|
|
ValueChanged="OnFontSizeChanged"
|
|
Variant="Variant.Outlined"
|
|
Margin="Margin.Dense"
|
|
Min="PrintPresetFilters.MinFontSizePt"
|
|
Max="PrintPresetFilters.MaxFontSizePt"
|
|
Style="max-width: 8rem;" />
|
|
<MudNumericField T="int"
|
|
Label="Answer lines"
|
|
Value="_answerSpaceLines"
|
|
ValueChanged="OnAnswerSpaceLinesChanged"
|
|
Variant="Variant.Outlined"
|
|
Margin="Margin.Dense"
|
|
Min="PrintPresetFilters.MinAnswerSpaceLines"
|
|
Max="PrintPresetFilters.MaxAnswerSpaceLines"
|
|
Style="max-width: 8rem;" />
|
|
@if (IsDirty())
|
|
{
|
|
<MudText Typo="Typo.caption" Color="Color.Warning">Unsaved changes</MudText>
|
|
}
|
|
@if (!_previewStale && _pages.Count > 0)
|
|
{
|
|
<MudText Typo="Typo.body2">@_pages.Count page@(_pages.Count == 1 ? "" : "s")</MudText>
|
|
}
|
|
else if (!_previewStale && _didPreview)
|
|
{
|
|
<MudText Typo="Typo.body2">No matches</MudText>
|
|
}
|
|
</MudStack>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12">
|
|
<MudText Typo="Typo.subtitle2" Class="mb-2">Template (Markdown)</MudText>
|
|
<div id="@EditorElementId" @key="_editorGeneration">
|
|
<MarkdownEditor Value="@_templateMarkdown"
|
|
ValueChanged="OnMarkdownChanged"
|
|
Placeholder="Write the printable page. Insert tokens for student, team, or event values."
|
|
AutoSaveEnabled="false"
|
|
NativeSpellChecker="false"
|
|
HideIcons="@HiddenEditorIcons" />
|
|
</div>
|
|
<MudText Typo="Typo.subtitle2" Class="mt-4 mb-1">Template preview</MudText>
|
|
<MudText Typo="Typo.caption" Class="mud-text-secondary mb-2">Markdown only — tokens are not merged here.</MudText>
|
|
@if (string.IsNullOrWhiteSpace(_templateMarkdown))
|
|
{
|
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">The template will preview here as you type.</MudText>
|
|
}
|
|
else
|
|
{
|
|
<MudPaper Elevation="0" Class="pa-3 note-print-page" Style="@($"background-color: var(--mud-palette-background-grey);{PrintPageStyle}")">
|
|
<div class="markdown-content">
|
|
@((MarkupString)MarkdownHelper.ToHtml(_templateMarkdown))
|
|
</div>
|
|
</MudPaper>
|
|
}
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
</div>
|
|
|
|
<div class="print-only">
|
|
<PrintPageStack Pages="_pages"
|
|
NewPagePerRecord="_newPagePerRecord"
|
|
PrintPageStyle="@PrintPageStyle" />
|
|
</div>
|
|
|
|
@code {
|
|
private const string EditorElementId = "page-printer-editor";
|
|
private static readonly string[] HiddenEditorIcons = ["preview", "side-by-side", "fullscreen"];
|
|
|
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
private bool _isDisposed;
|
|
private bool _isLoading = true;
|
|
private bool _isBusy;
|
|
private bool _previewStale = true;
|
|
private bool _didPreview;
|
|
private bool _pasteInitialized;
|
|
private int _editorGeneration;
|
|
|
|
private List<PrintPreset> _presets = [];
|
|
private List<string> _importedTokenNames = [];
|
|
private IReadOnlyList<NotePrintPage> _pages = [];
|
|
private IDialogReference? _previewDialog;
|
|
private IDisposable? _navigationRegistration;
|
|
|
|
private int? _selectedPresetId;
|
|
private string _templateMarkdown = string.Empty;
|
|
private PrintEntityType _entityType = PrintEntityType.Student;
|
|
private int? _grade;
|
|
private int? _tsaYear;
|
|
private string _officerChoice = "any";
|
|
private string? _teamIdentifierContains;
|
|
private string? _eventNameContains;
|
|
private EventFormat? _eventFormat;
|
|
private string _regionalChoice = "any";
|
|
private bool _newPagePerRecord = true;
|
|
private int _fontSizePt = PrintPresetFilters.DefaultFontSizePt;
|
|
private int _answerSpaceLines = PrintPresetFilters.DefaultAnswerSpaceLines;
|
|
|
|
private string _snapshotMarkdown = string.Empty;
|
|
private PrintEntityType _snapshotEntityType = PrintEntityType.Student;
|
|
private string _snapshotFiltersJson = string.Empty;
|
|
|
|
private string PrintPageStyle =>
|
|
$"--print-font-size:{_fontSizePt}pt;--print-answer-lines:{_answerSpaceLines};";
|
|
|
|
private bool CanPreview => !string.IsNullOrWhiteSpace(_templateMarkdown);
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
CaptureSnapshot();
|
|
_navigationRegistration = NavigationManager.RegisterLocationChangingHandler(OnLocationChanging);
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadAsync();
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (_pasteInitialized || _isDisposed)
|
|
return;
|
|
|
|
await Task.Delay(150);
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
await MarkdownTablePasteService.InitializeAsync(EditorElementId);
|
|
_pasteInitialized = true;
|
|
}
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
_isLoading = true;
|
|
try
|
|
{
|
|
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
await RefreshImportedTokenNamesAsync(token);
|
|
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
|
|
private async Task MarkStaleAsync()
|
|
{
|
|
_previewStale = true;
|
|
_pages = [];
|
|
await ClosePreviewDialogAsync();
|
|
}
|
|
|
|
private void MarkStale() => _ = MarkStaleAsync();
|
|
|
|
private async Task ClosePreviewDialogAsync()
|
|
{
|
|
var dialog = _previewDialog;
|
|
_previewDialog = null;
|
|
if (dialog is null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
dialog.Close();
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private async Task OnEntityTypeChanged(PrintEntityType value)
|
|
{
|
|
_entityType = value;
|
|
await MarkStaleAsync();
|
|
}
|
|
|
|
private void OnMarkdownChanged(string? value)
|
|
{
|
|
_templateMarkdown = value ?? string.Empty;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnGradeChanged(int? value)
|
|
{
|
|
_grade = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnTsaYearChanged(int? value)
|
|
{
|
|
_tsaYear = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnOfficerChanged(string value)
|
|
{
|
|
_officerChoice = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnTeamIdentifierChanged(string? value)
|
|
{
|
|
_teamIdentifierContains = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnEventNameChanged(string? value)
|
|
{
|
|
_eventNameContains = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnEventFormatChanged(EventFormat? value)
|
|
{
|
|
_eventFormat = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnRegionalChanged(string value)
|
|
{
|
|
_regionalChoice = value;
|
|
MarkStale();
|
|
}
|
|
|
|
private void OnNewPagePerRecordChanged(bool value)
|
|
{
|
|
_newPagePerRecord = value;
|
|
}
|
|
|
|
private void OnFontSizeChanged(int value)
|
|
{
|
|
_fontSizePt = Math.Clamp(value, PrintPresetFilters.MinFontSizePt, PrintPresetFilters.MaxFontSizePt);
|
|
}
|
|
|
|
private void OnAnswerSpaceLinesChanged(int value)
|
|
{
|
|
_answerSpaceLines = Math.Clamp(value, PrintPresetFilters.MinAnswerSpaceLines, PrintPresetFilters.MaxAnswerSpaceLines);
|
|
}
|
|
|
|
private async Task RefreshImportedTokenNamesAsync(CancellationToken token)
|
|
{
|
|
var indexFields = WebApp.Models.ChapterSettings.ReadIndexNoteFields(Configuration);
|
|
var discovered = await NotesService.GetImportedFieldNamesAsync(token);
|
|
_importedTokenNames =
|
|
[
|
|
.. indexFields
|
|
.Concat(discovered)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
|
|
];
|
|
}
|
|
|
|
private async Task FlushEditorAsync()
|
|
{
|
|
var value = await MarkdownTablePasteService.GetValueAsync(EditorElementId);
|
|
if (value is not null && value != _templateMarkdown)
|
|
{
|
|
_templateMarkdown = value;
|
|
await MarkStaleAsync();
|
|
}
|
|
}
|
|
|
|
private bool IsDirty()
|
|
{
|
|
return !string.Equals(_templateMarkdown, _snapshotMarkdown, StringComparison.Ordinal)
|
|
|| _entityType != _snapshotEntityType
|
|
|| !string.Equals(BuildFilters().ToJson(), _snapshotFiltersJson, StringComparison.Ordinal);
|
|
}
|
|
|
|
private void CaptureSnapshot()
|
|
{
|
|
_snapshotMarkdown = _templateMarkdown;
|
|
_snapshotEntityType = _entityType;
|
|
_snapshotFiltersJson = BuildFilters().ToJson();
|
|
}
|
|
|
|
private async Task<bool> ConfirmDiscardIfDirtyAsync()
|
|
{
|
|
await FlushEditorAsync();
|
|
if (!IsDirty())
|
|
return true;
|
|
|
|
var confirmed = await DialogService.ShowMessageBox(
|
|
"Unsaved changes",
|
|
"Discard unsaved template or filter changes?",
|
|
yesText: "Discard",
|
|
cancelText: "Stay");
|
|
|
|
return confirmed == true && !_isDisposed;
|
|
}
|
|
|
|
private async ValueTask OnLocationChanging(LocationChangingContext context)
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
if (!await ConfirmDiscardIfDirtyAsync())
|
|
context.PreventNavigation();
|
|
}
|
|
|
|
private async Task OnPresetSelected(int? id)
|
|
{
|
|
if (id == _selectedPresetId)
|
|
return;
|
|
|
|
if (!await ConfirmDiscardIfDirtyAsync())
|
|
return;
|
|
|
|
_selectedPresetId = id;
|
|
if (!id.HasValue)
|
|
{
|
|
await MarkStaleAsync();
|
|
return;
|
|
}
|
|
|
|
var preset = _presets.FirstOrDefault(p => p.Id == id.Value);
|
|
if (preset is null)
|
|
return;
|
|
|
|
ApplyPreset(preset);
|
|
CaptureSnapshot();
|
|
await MarkStaleAsync();
|
|
}
|
|
|
|
private async Task NewTemplate()
|
|
{
|
|
if (_isDisposed || !await ConfirmDiscardIfDirtyAsync())
|
|
return;
|
|
|
|
_selectedPresetId = null;
|
|
_templateMarkdown = string.Empty;
|
|
_entityType = PrintEntityType.Student;
|
|
_grade = null;
|
|
_tsaYear = null;
|
|
_officerChoice = "any";
|
|
_teamIdentifierContains = null;
|
|
_eventNameContains = null;
|
|
_eventFormat = null;
|
|
_regionalChoice = "any";
|
|
_newPagePerRecord = true;
|
|
_fontSizePt = PrintPresetFilters.DefaultFontSizePt;
|
|
_answerSpaceLines = PrintPresetFilters.DefaultAnswerSpaceLines;
|
|
_editorGeneration++;
|
|
_pasteInitialized = false;
|
|
CaptureSnapshot();
|
|
await MarkStaleAsync();
|
|
}
|
|
|
|
private void ApplyPreset(PrintPreset preset)
|
|
{
|
|
_entityType = preset.EntityType;
|
|
_templateMarkdown = preset.TemplateMarkdown ?? string.Empty;
|
|
var filters = PrintPresetFilters.FromJson(preset.FiltersJson);
|
|
_grade = filters.Grade;
|
|
_tsaYear = filters.TsaYear;
|
|
_officerChoice = PrintPresetFilters.ToTriState(filters.IsOfficer);
|
|
_teamIdentifierContains = filters.TeamIdentifierContains;
|
|
_eventNameContains = filters.EventNameContains;
|
|
_eventFormat = filters.EventFormat;
|
|
_regionalChoice = PrintPresetFilters.ToTriState(filters.RegionalOnly);
|
|
_newPagePerRecord = filters.NewPagePerRecord;
|
|
_fontSizePt = filters.FontSizePt;
|
|
_answerSpaceLines = filters.AnswerSpaceLines;
|
|
_editorGeneration++;
|
|
_pasteInitialized = false;
|
|
}
|
|
|
|
private PrintPresetFilters BuildFilters() =>
|
|
new()
|
|
{
|
|
Grade = _entityType == PrintEntityType.Student ? _grade : null,
|
|
TsaYear = _entityType == PrintEntityType.Student ? _tsaYear : null,
|
|
IsOfficer = _entityType == PrintEntityType.Student ? PrintPresetFilters.FromTriState(_officerChoice) : null,
|
|
TeamIdentifierContains = _entityType == PrintEntityType.Team ? _teamIdentifierContains : null,
|
|
EventNameContains = _entityType == PrintEntityType.Event ? _eventNameContains : null,
|
|
EventFormat = _entityType == PrintEntityType.Event ? _eventFormat : null,
|
|
RegionalOnly = _entityType == PrintEntityType.Event ? PrintPresetFilters.FromTriState(_regionalChoice) : null,
|
|
NewPagePerRecord = _newPagePerRecord,
|
|
FontSizePt = _fontSizePt,
|
|
AnswerSpaceLines = _answerSpaceLines
|
|
};
|
|
|
|
private async Task OpenTokenDialog()
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
await RefreshImportedTokenNamesAsync(token);
|
|
|
|
var parameters = new DialogParameters<PrintTokenInsertDialog>
|
|
{
|
|
{ x => x.EntityType, _entityType },
|
|
{ x => x.ImportedFieldNames, _importedTokenNames },
|
|
{ x => x.OnInsert, EventCallback.Factory.Create<string>(this, InsertToken) }
|
|
};
|
|
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.Large,
|
|
FullWidth = true,
|
|
CloseButton = true
|
|
};
|
|
|
|
await DialogService.ShowAsync<PrintTokenInsertDialog>("Insert token", parameters, options);
|
|
}
|
|
|
|
private async Task InsertToken(string tokenName)
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
await FlushEditorAsync();
|
|
var wrapped = "{{" + tokenName + "}}";
|
|
var inserted = await MarkdownTablePasteService.InsertAtCursorAsync(EditorElementId, wrapped);
|
|
if (!inserted)
|
|
{
|
|
_templateMarkdown += wrapped;
|
|
await MarkdownTablePasteService.SetValueAsync(EditorElementId, _templateMarkdown);
|
|
}
|
|
else
|
|
{
|
|
var value = await MarkdownTablePasteService.GetValueAsync(EditorElementId);
|
|
if (value is not null)
|
|
_templateMarkdown = value;
|
|
}
|
|
|
|
await MarkStaleAsync();
|
|
}
|
|
|
|
private async Task Preview()
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
await FlushEditorAsync();
|
|
if (!CanPreview)
|
|
{
|
|
if (!_isDisposed)
|
|
Snackbar.Add("Write a template before previewing.", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
if (!_previewStale && _didPreview)
|
|
{
|
|
await OpenPreviewDialogAsync();
|
|
return;
|
|
}
|
|
|
|
_isBusy = true;
|
|
try
|
|
{
|
|
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
await RefreshImportedTokenNamesAsync(token);
|
|
|
|
_pages = await NotePrintService.PreviewAsync(
|
|
new NotePrintRequest
|
|
{
|
|
EntityType = _entityType,
|
|
TemplateMarkdown = _templateMarkdown,
|
|
Filters = BuildFilters(),
|
|
ImportedFieldCatalog = _importedTokenNames
|
|
},
|
|
token);
|
|
|
|
_previewStale = false;
|
|
_didPreview = true;
|
|
|
|
if (!_isDisposed)
|
|
await OpenPreviewDialogAsync();
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Preview failed: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
_isBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task OpenPreviewDialogAsync()
|
|
{
|
|
await ClosePreviewDialogAsync();
|
|
|
|
var parameters = new DialogParameters<PrintPreviewDialog>
|
|
{
|
|
{ x => x.Pages, _pages },
|
|
{ x => x.NewPagePerRecord, _newPagePerRecord },
|
|
{ x => x.PrintPageStyle, PrintPageStyle },
|
|
{ x => x.OnPrint, EventCallback.Factory.Create(this, Print) }
|
|
};
|
|
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.ExtraLarge,
|
|
FullWidth = true,
|
|
CloseButton = true
|
|
};
|
|
|
|
var dialog = await DialogService.ShowAsync<PrintPreviewDialog>("Print preview", parameters, options);
|
|
_previewDialog = dialog;
|
|
_ = TrackPreviewDialog(dialog);
|
|
}
|
|
|
|
private async Task TrackPreviewDialog(IDialogReference dialog)
|
|
{
|
|
try
|
|
{
|
|
await dialog.Result;
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
if (_previewDialog == dialog)
|
|
_previewDialog = null;
|
|
}
|
|
}
|
|
|
|
private async Task Print()
|
|
{
|
|
if (_isDisposed || _previewStale || _pages.Count == 0)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("window.print");
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private async Task SavePreset()
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
await FlushEditorAsync();
|
|
if (!CanPreview)
|
|
{
|
|
if (!_isDisposed)
|
|
Snackbar.Add("Write a template before saving.", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
if (_selectedPresetId.HasValue)
|
|
{
|
|
await UpdateSelectedPresetAsync();
|
|
return;
|
|
}
|
|
|
|
var name = await PromptForPresetNameAsync();
|
|
if (string.IsNullOrWhiteSpace(name) || _isDisposed)
|
|
return;
|
|
|
|
_isBusy = true;
|
|
try
|
|
{
|
|
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
if (await PrintPresetService.NameExistsAsync(name, null, token))
|
|
{
|
|
Snackbar.Add($"A print preset named '{name}' already exists.", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
var created = await PrintPresetService.CreateAsync(
|
|
new PrintPreset
|
|
{
|
|
Name = name,
|
|
TemplateMarkdown = _templateMarkdown,
|
|
EntityType = _entityType,
|
|
FiltersJson = BuildFilters().ToJson()
|
|
},
|
|
token);
|
|
|
|
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
|
_selectedPresetId = created.Id;
|
|
CaptureSnapshot();
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Saved print preset '{name}'.", Severity.Success);
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Could not save: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
_isBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task<string?> PromptForPresetNameAsync()
|
|
{
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.Small,
|
|
FullWidth = true,
|
|
CloseButton = true
|
|
};
|
|
|
|
var dialog = await DialogService.ShowAsync<PrintPresetNameDialog>("Save print preset", options);
|
|
var result = await dialog.Result;
|
|
if (result is null || result.Canceled || result.Data is not string name)
|
|
return null;
|
|
|
|
return name.Trim();
|
|
}
|
|
|
|
private async Task UpdateSelectedPresetAsync()
|
|
{
|
|
if (_isDisposed || !_selectedPresetId.HasValue)
|
|
return;
|
|
|
|
_isBusy = true;
|
|
try
|
|
{
|
|
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
var existing = _presets.FirstOrDefault(p => p.Id == _selectedPresetId.Value);
|
|
if (existing is null)
|
|
return;
|
|
|
|
await PrintPresetService.UpdateAsync(
|
|
new PrintPreset
|
|
{
|
|
Id = existing.Id,
|
|
Name = existing.Name,
|
|
TemplateMarkdown = _templateMarkdown,
|
|
EntityType = _entityType,
|
|
FiltersJson = BuildFilters().ToJson()
|
|
},
|
|
token);
|
|
|
|
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
|
CaptureSnapshot();
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Saved print preset '{existing.Name}'.", Severity.Success);
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Could not save: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
_isBusy = false;
|
|
}
|
|
}
|
|
|
|
private async Task DeletePreset()
|
|
{
|
|
if (_isDisposed || !_selectedPresetId.HasValue)
|
|
return;
|
|
|
|
var existing = _presets.FirstOrDefault(p => p.Id == _selectedPresetId.Value);
|
|
if (existing is null)
|
|
return;
|
|
|
|
var confirmed = await DialogService.ShowMessageBox(
|
|
"Delete print preset",
|
|
(MarkupString)$"Delete <b>{existing.Name}</b>? This cannot be undone.",
|
|
yesText: "Delete",
|
|
cancelText: "Cancel");
|
|
|
|
if (confirmed != true || _isDisposed)
|
|
return;
|
|
|
|
_isBusy = true;
|
|
try
|
|
{
|
|
var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
await PrintPresetService.DeleteAsync(existing.Id, token);
|
|
_presets = [.. await PrintPresetService.GetAllAsync(token)];
|
|
_selectedPresetId = null;
|
|
CaptureSnapshot();
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Deleted print preset '{existing.Name}'.", Severity.Info);
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
Snackbar.Add($"Could not delete: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
_isBusy = false;
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
_isDisposed = true;
|
|
_navigationRegistration?.Dispose();
|
|
_navigationRegistration = null;
|
|
await ClosePreviewDialogAsync();
|
|
_cancellationTokenSource?.Cancel();
|
|
_cancellationTokenSource?.Dispose();
|
|
_cancellationTokenSource = null;
|
|
}
|
|
|
|
await ValueTask.CompletedTask;
|
|
}
|
|
}
|