From 6cad6bc3a14b8bc39cba5c1580c62e4f33379d15 Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Wed, 2 Jul 2025 16:41:09 -0500 Subject: [PATCH] More UI mockups, and added an szf-editor (need to update the spec on it though) --- .../Datasets/DatasetEditor.razor | 0 .../Datasets/DatasetEditor.razor.cs | 2 +- .../Datasets/DatasetEditor.razor.css | 0 SessionZero/Pages/Characters.razor | 6 - SessionZero/Pages/Characters/Characters.razor | 17 + .../Pages/Characters/Characters.razor.cs | 6 + .../CharacterTemplates.razor | 73 ++ .../CharacterTemplates.razor.cs | 23 + .../CharacterTemplates/Templates.razor | 35 - .../Pages/Library/Datasets/Datasets.razor | 1 + SessionZero/Pages/SzfEditor.razor | 744 ++++++++++++++++++ SessionZero/Pages/SzfParseTest.razor | 2 +- SessionZero/SessionZero.csproj | 1 - 13 files changed, 866 insertions(+), 44 deletions(-) rename SessionZero/{Pages/Library => Components}/Datasets/DatasetEditor.razor (100%) rename SessionZero/{Pages/Library => Components}/Datasets/DatasetEditor.razor.cs (69%) rename SessionZero/{Pages/Library => Components}/Datasets/DatasetEditor.razor.css (100%) delete mode 100644 SessionZero/Pages/Characters.razor create mode 100644 SessionZero/Pages/Characters/Characters.razor create mode 100644 SessionZero/Pages/Characters/Characters.razor.cs create mode 100644 SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor create mode 100644 SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor.cs delete mode 100644 SessionZero/Pages/Library/CharacterTemplates/Templates.razor create mode 100644 SessionZero/Pages/SzfEditor.razor diff --git a/SessionZero/Pages/Library/Datasets/DatasetEditor.razor b/SessionZero/Components/Datasets/DatasetEditor.razor similarity index 100% rename from SessionZero/Pages/Library/Datasets/DatasetEditor.razor rename to SessionZero/Components/Datasets/DatasetEditor.razor diff --git a/SessionZero/Pages/Library/Datasets/DatasetEditor.razor.cs b/SessionZero/Components/Datasets/DatasetEditor.razor.cs similarity index 69% rename from SessionZero/Pages/Library/Datasets/DatasetEditor.razor.cs rename to SessionZero/Components/Datasets/DatasetEditor.razor.cs index e88103e..18e6411 100644 --- a/SessionZero/Pages/Library/Datasets/DatasetEditor.razor.cs +++ b/SessionZero/Components/Datasets/DatasetEditor.razor.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; -namespace SessionZero.Pages.Library.Datasets; +namespace SessionZero.Components.Datasets; public partial class DatasetEditor : ComponentBase { diff --git a/SessionZero/Pages/Library/Datasets/DatasetEditor.razor.css b/SessionZero/Components/Datasets/DatasetEditor.razor.css similarity index 100% rename from SessionZero/Pages/Library/Datasets/DatasetEditor.razor.css rename to SessionZero/Components/Datasets/DatasetEditor.razor.css diff --git a/SessionZero/Pages/Characters.razor b/SessionZero/Pages/Characters.razor deleted file mode 100644 index 48302de..0000000 --- a/SessionZero/Pages/Characters.razor +++ /dev/null @@ -1,6 +0,0 @@ -@page "/Characters" -

Characters

- -@code { - -} \ No newline at end of file diff --git a/SessionZero/Pages/Characters/Characters.razor b/SessionZero/Pages/Characters/Characters.razor new file mode 100644 index 0000000..b99631d --- /dev/null +++ b/SessionZero/Pages/Characters/Characters.razor @@ -0,0 +1,17 @@ +@page "/Characters" +@inject NavigationManager NavigationManager + +

Characters

+ +
+ + +

Create

+

Create a new Character

+
+ + +

Manage

+

View and manage saved Characters

+
+
\ No newline at end of file diff --git a/SessionZero/Pages/Characters/Characters.razor.cs b/SessionZero/Pages/Characters/Characters.razor.cs new file mode 100644 index 0000000..fbc34b5 --- /dev/null +++ b/SessionZero/Pages/Characters/Characters.razor.cs @@ -0,0 +1,6 @@ +namespace SessionZero.Pages.Characters; + +public partial class Characters +{ + +} \ No newline at end of file diff --git a/SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor b/SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor new file mode 100644 index 0000000..6982e84 --- /dev/null +++ b/SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor @@ -0,0 +1,73 @@ +@page "/library/character-templates" + + + +@switch (_currentPageState) +{ + case PageState.Main: + + + Back to Library + +

Character Templates

+
+ +
+ +

Create

+

Create a new template

+
+ +
+ +

Manage

+

Manage and edit saved templates

+
+ +
+ +

SessionZeroDB

+

Search online for new templates (NOT FUNCTIONAL)

+
+
+ break; + case PageState.Create: +
+ + Go Back +
+

Create new Character

+

Here you can create a new character template

+ break; + case PageState.List: +
+ + Go Back +
+

Characters

+

Here you can manage your saved character templates

+ break; + case PageState.SessionZeroDb: +
+ + Go Back +
+

Create new Character

+

Here you will be able to search for and download new character templates

+ break; + default: +
+ + Go Back +
+

Oh no!

+

You're not supposed to be here! Go back and try again.

+ break; +} + + + + + + + diff --git a/SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor.cs b/SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor.cs new file mode 100644 index 0000000..2e91e13 --- /dev/null +++ b/SessionZero/Pages/Library/CharacterTemplates/CharacterTemplates.razor.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Components; + +namespace SessionZero.Pages.Library.CharacterTemplates; + +public partial class CharacterTemplates : ComponentBase +{ + private PageState _currentPageState = PageState.Main; + + private enum PageState + { + Main, + Create, + View, + List, + Edit, + SessionZeroDb + } + + private void SetPageState(PageState state) + { + _currentPageState = state; + } +} \ No newline at end of file diff --git a/SessionZero/Pages/Library/CharacterTemplates/Templates.razor b/SessionZero/Pages/Library/CharacterTemplates/Templates.razor deleted file mode 100644 index 02bbec8..0000000 --- a/SessionZero/Pages/Library/CharacterTemplates/Templates.razor +++ /dev/null @@ -1,35 +0,0 @@ -@page "/library/character-templates" - - - - Back to Library - - -

Character Templates

- -
- -
- -

Create

-

Create a new template

-
- -
- -

Manage

-

Manage and edit saved templates

-
- -
- -

SessionZeroDB

-

Search online for new templates (NOT FUNCTIONAL)

-
- - -
- -@code { - -} \ No newline at end of file diff --git a/SessionZero/Pages/Library/Datasets/Datasets.razor b/SessionZero/Pages/Library/Datasets/Datasets.razor index f6a0065..de13c03 100644 --- a/SessionZero/Pages/Library/Datasets/Datasets.razor +++ b/SessionZero/Pages/Library/Datasets/Datasets.razor @@ -1,6 +1,7 @@ @page "/Library/Datasets" @using SessionZero.Data @using SessionZero.Models +@using SessionZero.Components.Datasets @inject NavigationManager NavigationManager @inject SessionZero.Services.ISzfStorageService SzfStorageService @inject SzfParser SzfParser diff --git a/SessionZero/Pages/SzfEditor.razor b/SessionZero/Pages/SzfEditor.razor new file mode 100644 index 0000000..e65c245 --- /dev/null +++ b/SessionZero/Pages/SzfEditor.razor @@ -0,0 +1,744 @@ +@page "/szf-editor" +@using System.Text +@using System.Text.RegularExpressions +@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime + +SZF Editor + +
+
+ + + + +
+ +
+
+
+
+ @for (int i = 1; i <= _lines.Count; i++) + { +
@i
+ } +
+
+ +
+ @foreach (var line in _highlightedLines) + { +
@((MarkupString)line)
+ } +
+
+
+
+ @if (_errors.Any()) + { + @foreach (var error in _errors) + { +
Line @(error.Line + 1): @error.Message
+ } + } + else + { +
No errors.
+ } +
+
+ +
+
+ @if (_parsedStructure != null) + { + @RenderStructure(_parsedStructure) + } +
+
+
+
+ +@code { + private ElementReference editorElement; + private ElementReference textareaRef; + private ElementReference overlayRef; + private InputFile fileInput = null!; + + private string _content = ""; + private List _lines = new(); + private List _highlightedLines = new(); + private List _errors = new(); + private Dictionary? _parsedStructure; + private bool _showConsole = true; + + private static readonly HashSet ValidTypes = new() + { + "text", "text-field", "number", "bool", "group", "calculated", + "system", "dataset-reference", "dataset-type", + "dataset-reference-multiple", "dataset-type-multiple" + }; + + protected override void OnInitialized() + { + var exampleSzf = @"!type: character_template +!schema: 1.0.0 +bad line here + +# This is a comment. It will be ignored by the parser. +# It's useful for adding notes to your templates. + +[Metadata] +Name (text) = Robust Fantasy Character +Author (text) = Gemini +GameSystem (text) = Fantasy RPG Universal +Description (text-field) = A character sheet with calculated values and dependencies. +InvalidType (faketype) = some value + +[Required_Datasets] +Classes (dataset-reference) = Core Classes|c3d4e5f6-g7h8-9012-cdef-123456789012|1.5.0 + +[Section: Ability Scores] +Strength (number) = 14 +Dexterity (number) = 12 +Constitution (number) = 15 +IsCool (bool) = true + +[Section.Ability_Scores.Modifiers] +# Modifiers are calculated automatically. +StrengthMod (calculated) = (Strength - 10) / 2 +DexterityMod (calculated) = (Dexterity - 10) / 2 +ConstitutionMod (calculated) = (Constitution - 10) / 2 + +[Section: Combat] +ProficiencyBonus (number) = 2 +HitPoints (calculated) = 10 + ConstitutionMod +ArmorClass (calculated) = 10 + DexterityMod +"; + + SetContent(exampleSzf); + } + + private void SetContent(string content) + { + _content = content; + _lines = content.Split('\n').ToList(); + UpdateSyntaxHighlighting(); + ParseContent(); + } + + private void UpdateSyntaxHighlighting() + { + _highlightedLines.Clear(); + + foreach (var line in _lines) + { + _highlightedLines.Add(HighlightLine(line)); + } + + StateHasChanged(); + } + + private string HighlightLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) + return " "; + + var trimmed = line.Trim(); + + // Comments + if (trimmed.StartsWith("#")) + { + return $"{System.Web.HttpUtility.HtmlEncode(line)}"; + } + + // Metadata + if (trimmed.StartsWith("!")) + { + var parts = line.Split(':', 2); + if (parts.Length == 2) + { + var key = System.Web.HttpUtility.HtmlEncode(parts[0]); + var value = System.Web.HttpUtility.HtmlEncode(parts[1]); + return $"{key}:{value}"; + } + return $"{System.Web.HttpUtility.HtmlEncode(line)}"; + } + + // Section headers + if (trimmed.StartsWith("[") && trimmed.EndsWith("]")) + { + return $"{System.Web.HttpUtility.HtmlEncode(line)}"; + } + + // Field definitions + var fieldMatch = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$"); + if (fieldMatch.Success) + { + var fieldName = fieldMatch.Groups[1].Value; + var fieldType = fieldMatch.Groups[2].Value; + var fieldValue = fieldMatch.Groups[3].Value; + + var leadingSpaces = line.Length - line.TrimStart().Length; + var spaces = leadingSpaces > 0 ? new string(' ', leadingSpaces) : ""; + + var highlightedName = $"{System.Web.HttpUtility.HtmlEncode(fieldName)}"; + var highlightedType = $"({System.Web.HttpUtility.HtmlEncode(fieldType)})"; + var highlightedOperator = $" = "; + + string highlightedValue; + if (fieldType == "calculated") + { + highlightedValue = HighlightCalculatedValue(fieldValue); + } + else if (fieldType == "number" && Regex.IsMatch(fieldValue.Trim(), @"^-?\d+(\.\d+)?$")) + { + highlightedValue = $"{System.Web.HttpUtility.HtmlEncode(fieldValue)}"; + } + else if (fieldType == "bool" && Regex.IsMatch(fieldValue.Trim(), @"^(true|false)$", RegexOptions.IgnoreCase)) + { + highlightedValue = $"{System.Web.HttpUtility.HtmlEncode(fieldValue)}"; + } + else + { + highlightedValue = $"{System.Web.HttpUtility.HtmlEncode(fieldValue)}"; + } + + return $"{spaces}{highlightedName} {highlightedType}{highlightedOperator}{highlightedValue}"; + } + + return System.Web.HttpUtility.HtmlEncode(line); + } + + private string HighlightCalculatedValue(string value) + { + var result = new StringBuilder(); + var tokens = Regex.Split(value, @"(\+|\-|\*|\/|\(|\)|[\w\d_]+|\d+(?:\.\d+)?|\s+)"); + + foreach (var token in tokens) + { + if (string.IsNullOrEmpty(token)) continue; + + if (Regex.IsMatch(token, @"^[+\-*/()]$")) + { + result.Append($"{System.Web.HttpUtility.HtmlEncode(token)}"); + } + else if (Regex.IsMatch(token, @"^-?\d+(\.\d+)?$")) + { + result.Append($"{System.Web.HttpUtility.HtmlEncode(token)}"); + } + else if (Regex.IsMatch(token, @"^[A-Za-z_][\w\d_]*$")) + { + result.Append($"{System.Web.HttpUtility.HtmlEncode(token)}"); + } + else + { + result.Append(System.Web.HttpUtility.HtmlEncode(token)); + } + } + + return result.ToString(); + } + + private void ParseContent() + { + var result = ParseSzf(_content); + _parsedStructure = result.Structure; + _errors = result.Errors; + } + + private (Dictionary Structure, List Errors) ParseSzf(string text) + { + var lines = text.Split('\n'); + var structure = new Dictionary(); + var current = structure; + var path = new List(); + var errors = new List(); + + for (int idx = 0; idx < lines.Length; idx++) + { + var line = lines[idx]; + var trimmed = line.Trim(); + + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")) + continue; + + if (trimmed.StartsWith("!")) + { + var parts = trimmed.Substring(1).Split(':', 2); + if (parts.Length < 2) + { + errors.Add(new SzfError(idx, "Invalid metadata: Missing value after ':'")); + } + else + { + structure[parts[0].Trim()] = parts[1].Trim(); + } + } + else if (trimmed.StartsWith("[") && trimmed.EndsWith("]")) + { + var sectionPath = trimmed.Substring(1, trimmed.Length - 2).Replace(": ", "."); + path = sectionPath.Split('.').Where(p => !string.IsNullOrWhiteSpace(p)).ToList(); + + current = structure; + foreach (var p in path) + { + var cleanP = p.Trim(); + if (!string.IsNullOrEmpty(cleanP)) + { + if (!current.ContainsKey(cleanP)) + current[cleanP] = new Dictionary(); + current = (Dictionary)current[cleanP]; + } + } + } + else + { + var match = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$"); + if (!match.Success) + { + if (!string.IsNullOrEmpty(trimmed)) + errors.Add(new SzfError(idx, "Invalid field definition. Expected format: FieldName (type) = value")); + continue; + } + + var name = match.Groups[1].Value; + var type = match.Groups[2].Value; + var value = match.Groups[3].Value.Trim(); + + if (!ValidTypes.Contains(type)) + { + errors.Add(new SzfError(idx, $"Unknown field type: \"{type}\"")); + } + + current[name] = new Dictionary { ["type"] = type, ["value"] = value }; + } + } + + return (structure, errors); + } + + private bool HasErrorOnLine(int lineIndex) + { + return _errors.Any(e => e.Line == lineIndex); + } + + private RenderFragment RenderStructure(Dictionary structure) + { + return builder => + { + int sequence = 0; + foreach (var kvp in structure) + { + if (kvp.Value is string stringValue) + { + builder.OpenElement(sequence++, "div"); + builder.OpenElement(sequence++, "strong"); + builder.AddContent(sequence++, $"{kvp.Key}:"); + builder.CloseElement(); + builder.AddContent(sequence++, $" {stringValue}"); + builder.CloseElement(); + } + else if (kvp.Value is Dictionary dict) + { + RenderSection(builder, ref sequence, dict, kvp.Key, true); + } + } + }; + } + + private void RenderSection(RenderTreeBuilder builder, ref int sequence, Dictionary obj, string title, bool isTopLevel = false) + { + var sectionContent = new Dictionary(); + var subsections = new Dictionary(); + + foreach (var kvp in obj) + { + if (kvp.Value is Dictionary dict && !dict.ContainsKey("type")) + { + subsections[kvp.Key] = dict; + } + else + { + sectionContent[kvp.Key] = kvp.Value; + } + } + + var hasContent = sectionContent.Any(); + if (hasContent || isTopLevel) + { + builder.OpenElement(sequence++, "div"); + builder.AddAttribute(sequence++, "class", "section"); + + builder.OpenElement(sequence++, "h3"); + builder.AddContent(sequence++, title.Replace("_", " ")); + builder.CloseElement(); + + if (hasContent) + { + builder.OpenElement(sequence++, "table"); + builder.OpenElement(sequence++, "thead"); + builder.OpenElement(sequence++, "tr"); + builder.OpenElement(sequence++, "th"); + builder.AddContent(sequence++, "Name"); + builder.CloseElement(); + builder.OpenElement(sequence++, "th"); + builder.AddContent(sequence++, "Type"); + builder.CloseElement(); + builder.OpenElement(sequence++, "th"); + builder.AddContent(sequence++, "Value"); + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + + builder.OpenElement(sequence++, "tbody"); + foreach (var kvp in sectionContent) + { + if (kvp.Value is Dictionary field && + field.ContainsKey("type") && field.ContainsKey("value")) + { + builder.OpenElement(sequence++, "tr"); + + builder.OpenElement(sequence++, "td"); + builder.AddAttribute(sequence++, "class", "field"); + builder.AddContent(sequence++, kvp.Key); + builder.CloseElement(); + + builder.OpenElement(sequence++, "td"); + builder.AddAttribute(sequence++, "class", "type"); + builder.AddContent(sequence++, field["type"].ToString()); + builder.CloseElement(); + + builder.OpenElement(sequence++, "td"); + builder.AddAttribute(sequence++, "class", "value"); + builder.AddContent(sequence++, field["value"].ToString()); + builder.CloseElement(); + + builder.CloseElement(); + } + } + builder.CloseElement(); + builder.CloseElement(); + } + else if (isTopLevel && !subsections.Any()) + { + builder.AddContent(sequence++, "No entries."); + } + + builder.CloseElement(); + } + + foreach (var kvp in subsections) + { + if (kvp.Value is Dictionary subDict) + { + RenderSection(builder, ref sequence, subDict, $"{title}.{kvp.Key}"); + } + } + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + await Task.Delay(1); // Allow the input to be processed + await InvokeAsync(() => + { + _lines = _content.Split('\n').ToList(); + UpdateSyntaxHighlighting(); + ParseContent(); + }); + } + + private async Task HandleScroll() + { + // Sync scroll between textarea and overlay + await JSRuntime.InvokeVoidAsync("syncScroll", textareaRef, overlayRef); + } + + private async Task OpenFile() + { + var element = fileInput.Element; + if (element.HasValue) + { + await JSRuntime.InvokeVoidAsync("triggerFileInput", element.Value); + } + } + + private async Task HandleFileSelected(InputFileChangeEventArgs e) + { + var file = e.File; + if (file != null) + { + using var reader = new StreamReader(file.OpenReadStream()); + var content = await reader.ReadToEndAsync(); + SetContent(content); + } + } + + private async Task SaveFile() + { + var fileName = "file.szf"; + var content = _content; + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, content); + } + + private void ToggleConsole() + { + _showConsole = !_showConsole; + } + + public class SzfError + { + public int Line { get; } + public string Message { get; } + + public SzfError(int line, string message) + { + Line = line; + Message = message; + } + } +} + + + + \ No newline at end of file diff --git a/SessionZero/Pages/SzfParseTest.razor b/SessionZero/Pages/SzfParseTest.razor index a338e5b..b3bb46d 100644 --- a/SessionZero/Pages/SzfParseTest.razor +++ b/SessionZero/Pages/SzfParseTest.razor @@ -1,4 +1,4 @@ -@page "/szf" +@page "/szf-parser-test" @using SessionZero.Data @using SessionZero.Services @using SessionZero.Models diff --git a/SessionZero/SessionZero.csproj b/SessionZero/SessionZero.csproj index 4fda865..36606e7 100644 --- a/SessionZero/SessionZero.csproj +++ b/SessionZero/SessionZero.csproj @@ -23,7 +23,6 @@ -