Begin on dataset management screens

This commit is contained in:
Chris Bell 2025-07-01 22:21:04 -05:00
parent 51ec6a480d
commit 86ed6122c9
10 changed files with 617 additions and 78 deletions

View File

@ -0,0 +1,35 @@
@page "/library/character-templates"
<NavLink class="button-secondary" href="/library/" style="align-self: flex-start;">
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
Back to Library
</NavLink>
<h1 class="page-title">Character Templates</h1>
<div class="library-card-container">
<div class="card" onclick="@(() => { })">
<img src="res/icons/outline/general/plus.svg" class="icon"/>
<h1>Create</h1>
<p>Create a new template</p>
</div>
<div class="card" onclick="@(() => { })">
<img src="res/icons/outline/general/edit.svg" class="icon"/>
<h1>Manage</h1>
<p>Manage and edit saved templates</p>
</div>
<div class="card" onclick="@(() => { })">
<img src="res/icons/outline/general/search.svg" class="icon"/>
<h1>SessionZeroDB</h1>
<p>Search online for new templates (NOT FUNCTIONAL)</p>
</div>
</div>
@code {
}

View File

@ -1,10 +0,0 @@
@page "/Library/Datasets"
<NavLink class="button-secondary" href="/library/" style="align-self: flex-start;">
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
Back to Library
</NavLink>
<h1 class="page-title">Datasets</h1>
<p>@_message</p>

View File

@ -1,8 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace SessionZero.Pages.Library;
public partial class Datasets : ComponentBase
{
private string _message = "Datasets Page Loaded Successfully!";
}

View File

@ -0,0 +1,275 @@
@page "/Library/Datasets"
@using SessionZero.Data
@using SessionZero.Models
@inject NavigationManager NavigationManager
@inject SessionZero.Services.ISzfStorageService SzfStorageService
@inject SzfParser SzfParser
@inject SzfGenerator SzfGenerator
<NavLink class="button-secondary" href="/library/" style="align-self: flex-start;">
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
Back to Library
</NavLink>
<h1 class="page-title">Datasets</h1>
@if (!string.IsNullOrEmpty(_message))
{
<div class="panel" style="background-color: var(--primary-color-light); margin-bottom: 1rem;">
<p>@_message</p>
</div>
}
@switch (CurrentView)
{
case DatasetView.Overview:
<div class="library-card-container">
<div class="card" @onclick="ShowCreateDataset">
<img src="res/icons/outline/general/plus.svg" class="icon"/>
<h1>Create</h1>
<p>Create a new dataset</p>
</div>
<div class="card" @onclick="ShowManageDatasets">
<img src="res/icons/outline/general/edit.svg" class="icon"/>
<h1>Manage</h1>
<p>Manage and edit saved datasets</p>
</div>
<div class="card">
<img src="res/icons/outline/general/search.svg" class="icon"/>
<h1>SessionZeroDB</h1>
<p>Search online for new datasets (NOT FUNCTIONAL)</p>
</div>
</div>
break;
case DatasetView.CreateDataset:
<h3>Create New Dataset</h3>
<div class="panel">
<EditForm Model="@NewDataset" OnValidSubmit="@HandleCreateDataset">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label class="form-label" for="datasetName">Name</label>
<InputText id="datasetName" class="form-control" @bind-Value="NewDataset.Metadata.Name" />
</div>
<div class="form-group">
<label class="form-label" for="datasetType">Type (e.g., items, spells)</label>
<InputText id="datasetType" class="form-control" @bind-Value="NewDataset.DatasetType" />
</div>
<div class="form-group">
<label class="form-label" for="datasetVersion">Version (e.g., 1.0.0)</label>
<InputText id="datasetVersion" class="form-control" @bind-Value="DatasetVersionString" />
</div>
<div class="form-group">
<label class="form-label" for="datasetAuthor">Author</label>
<InputText id="datasetAuthor" class="form-control" @bind-Value="NewDataset.Metadata.Author" />
</div>
<div class="form-group">
<label class="form-label" for="datasetImageUrl">Image URL</label>
<InputText id="datasetImageUrl" class="form-control" @bind-Value="NewDataset.ImageUrl" />
</div>
<div class="form-group">
<label class="form-label" for="datasetDescription">Description</label>
<InputTextArea id="datasetDescription" class="form-control" @bind-Value="NewDataset.Metadata.Description" />
</div>
<div class="button-group">
<button type="submit" class="button-main">Create Dataset</button>
<button type="button" class="button-secondary" @onclick="ShowOverview">Cancel</button>
</div>
</EditForm>
</div>
break;
case DatasetView.ManageDatasets:
<h3>Manage Datasets</h3>
@if (SavedDatasets == null)
{
<p>Loading datasets...</p>
}
else if (!SavedDatasets.Any())
{
<div class="panel">
<p>No datasets saved yet. Click "Create" to add one.</p>
</div>
}
else
{
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Version</th>
<th>Author</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var dataset in SavedDatasets)
{
<tr>
<td>@dataset.Metadata.Name</td>
<td>@dataset.DatasetType</td>
<td>@dataset.Metadata.Version</td>
<td>@dataset.Metadata.Author</td>
<td>
<div class="button-group">
<button class="button-main" @onclick="() => EditDataset(dataset.Metadata.Guid.ToString())">Edit</button>
<button class="button-danger" @onclick="() => DeleteDataset(dataset.Metadata.Guid.ToString())">Delete</button>
</div>
</td>
</tr>
}
</tbody>
</table>
}
<div class="button-group">
<button type="button" class="button-secondary" @onclick="ShowOverview">Back</button>
</div>
break;
}
@code {
private enum DatasetView { Overview, CreateDataset, ManageDatasets }
private DatasetView CurrentView = DatasetView.Overview;
private SessionZero.Data.Dataset NewDataset = new();
private IEnumerable<SessionZero.Data.Dataset>? SavedDatasets;
private string _message = string.Empty;
// New property to handle Version binding
private string DatasetVersionString
{
get => NewDataset.Metadata.Version?.ToString() ?? string.Empty;
set
{
if (Version.TryParse(value, out var parsedVersion))
{
NewDataset.Metadata.Version = parsedVersion;
}
else
{
// Handle invalid version string, e.g., set to null or a default, or show an error
// For simplicity, we'll just leave it as is if parsing fails.
// A better approach would be to use a CustomValidator or more robust input handling.
}
}
}
protected override async Task OnInitializedAsync()
{
await LoadDatasets();
}
private void ShowOverview()
{
CurrentView = DatasetView.Overview;
_message = string.Empty; // Clear any messages
NewDataset = new(); // Reset the new dataset form
StateHasChanged();
}
private void ShowCreateDataset()
{
CurrentView = DatasetView.CreateDataset;
_message = string.Empty;
NewDataset = new() // Initialize with default values if needed
{
Metadata = new()
{
Guid = Guid.NewGuid(),
Version = new Version(1, 0, 0) // Ensure Version is initialized
}
};
StateHasChanged();
}
private async Task ShowManageDatasets()
{
CurrentView = DatasetView.ManageDatasets;
_message = string.Empty;
await LoadDatasets(); // Reload datasets when entering management view
StateHasChanged();
}
private async Task HandleCreateDataset()
{
try
{
var validationResult = NewDataset.Validate();
if (!validationResult.IsValid)
{
_message = "Validation errors: " + string.Join("; ", validationResult.Errors);
return;
}
SzfFile szfFile = new SzfFile
{
Guid = NewDataset.Metadata.Guid.ToString(),
Content = SzfGenerator.Generate(NewDataset),
Type = "dataset",
};
await SzfStorageService.SaveSzfFileAsync(szfFile);
_message = $"Dataset '{NewDataset.Metadata.Name}' created successfully!";
await LoadDatasets(); // Refresh the list of datasets
ShowOverview(); // Go back to overview after creation
}
catch (Exception ex)
{
_message = $"Error creating dataset: {ex.Message}";
}
}
private async Task LoadDatasets()
{
try
{
var allSzfDatasetFiles = await SzfStorageService.GetSzfFilesByTypeAsync("dataset");
var allSzfObjects = allSzfDatasetFiles.Select(file => SzfParser.Parse(file.Content));
SavedDatasets = allSzfObjects.OfType<SessionZero.Data.Dataset>().ToList();
}
catch (Exception ex)
{
_message = $"Error loading datasets: {ex.Message}";
SavedDatasets = new List<Dataset>(); // Ensure it's not null on error
}
StateHasChanged();
}
private async Task EditDataset(string guid)
{
_message = $"Attempting to edit dataset with GUID: {guid}";
var datasetFileToEdit = await SzfStorageService.GetSzfFileByGuidAsync(guid);
var datasetToEdit = SzfParser.Parse(datasetFileToEdit.Content);
if (datasetToEdit is Dataset ds)
{
NewDataset = ds; // Populate the form with the dataset to edit
CurrentView = DatasetView.CreateDataset; // Re-use the create form for editing
_message = $"Editing dataset: {ds.Metadata.Name}";
}
else
{
_message = "Dataset not found for editing.";
}
StateHasChanged();
}
private async Task DeleteDataset(string guid)
{
try
{
await SzfStorageService.DeleteSzfFileAsync(guid);
_message = "Dataset deleted successfully!";
await LoadDatasets(); // Refresh the list after deletion
}
catch (Exception ex)
{
_message = $"Error deleting dataset: {ex.Message}";
}
StateHasChanged();
}
}

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace SessionZero.Pages.Library.Datasets;
public partial class Datasets : ComponentBase
{
}

View File

@ -1,9 +1,11 @@
@page "/Library" @page "/Library"
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<h1 class="page-title">Library</h1> <h1 class="page-title">Library</h1>
<div class="library-card-container"> <div class="library-card-container">
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/library/templates"); })"> <NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/library/character-templates"); })">
<img src="res/icons/outline/user/address-book.svg" class="icon" /> <img src="res/icons/outline/user/address-book.svg" class="icon" />
<h1>Character Sheet Templates</h1> <h1>Character Sheet Templates</h1>
<p>Create and manage Character Sheet Templates</p> <p>Create and manage Character Sheet Templates</p>
@ -14,39 +16,3 @@
<p>Create and manage Datasets</p> <p>Create and manage Datasets</p>
</NavLink> </NavLink>
</div> </div>
<style>
.library-card-container {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: flex-start;
}
.library-card-container .card {
flex: 1;
min-width: 250px;
max-width: calc(50% - 0.75rem);
padding: 1.5rem;
}
.library-card-container .card p {
color: var(--neutral-medium);
}
.library-card-container .card h1 {
font-size: 1.6em;
color: var(--heading-color);
font-weight: normal;
}
</style>
@code {
private void NavigateToTemplates()
{
}
}

View File

@ -1,12 +0,0 @@
@page "/library/templates"
<NavLink class="button-secondary" href="/library/" style="align-self: flex-start;">
<img src="res/icons/outline/arrows/arrow-left.svg" class="button-icon" />
Back to Library
</NavLink>
<h1 class="page-title">Templates</h1>
@code {
}

View File

@ -100,6 +100,30 @@ html, body, #app {
font-size: 0.9rem; font-size: 0.9rem;
} }
.library-card-container {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: flex-start;
}
.library-card-container .card {
flex: 1;
min-width: 250px;
max-width: calc(50% - 0.75rem);
padding: 1.5rem;
}
.library-card-container .card p {
color: var(--neutral-medium);
}
.library-card-container .card h1 {
font-size: 1.6em;
color: var(--heading-color);
font-weight: normal;
}
.button-main { .button-main {
background-color: var(--primary-color); background-color: var(--primary-color);
padding: 0.6rem 1.2rem; padding: 0.6rem 1.2rem;
@ -145,6 +169,267 @@ html, body, #app {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
/* Forms and Input Elements */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
color: var(--heading-color);
font-weight: 600;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
background-color: var(--form-background);
border: 1px solid var(--primary-color);
border-radius: 4px;
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(91, 137, 179, 0.3); /* Accent color with transparency */
}
.form-control::placeholder {
color: var(--neutral-medium);
}
textarea.form-control {
min-height: 100px;
resize: vertical;
}
select.form-control {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url('../res/icons/arrow-left.svg'); /* Using local arrow-left.svg */
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1em;
padding-right: 2.5rem; /* Make space for the arrow */
}
/* Checkboxes and Radios */
.form-check {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.form-check-input {
margin-right: 0.75rem;
width: 1.25rem;
height: 1.25rem;
border: 1px solid var(--primary-color-light);
border-radius: 3px;
background-color: var(--form-background);
cursor: pointer;
position: relative;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.form-check-input:checked {
background-color: var(--accent-color);
border-color: var(--accent-color);
}
.form-check-input[type="checkbox"]:checked::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 6px;
height: 10px;
border: solid var(--text-color);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.form-check-input[type="radio"] {
border-radius: 50%;
}
.form-check-input[type="radio"]:checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-color);
transform: translate(-50%, -50%);
}
.form-check-label {
color: var(--text-color);
cursor: pointer;
font-size: 1rem;
}
/* Tables */
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 1.5rem;
background-color: var(--secondary-color);
border-radius: 8px;
overflow: hidden; /* Ensures rounded corners apply to content */
}
.data-table th,
.data-table td {
padding: 1rem 1.2rem;
text-align: left;
border-bottom: 1px solid var(--neutral-dark);
}
.data-table thead {
background-color: var(--primary-color);
}
.data-table th {
color: var(--heading-color);
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
}
.data-table tbody tr {
transition: background-color 0.2s ease;
}
.data-table tbody tr:nth-child(even) {
background-color: var(--secondary-color);
}
.data-table tbody tr:nth-child(odd) {
background-color: var(--form-background);
}
.data-table tbody tr:hover {
background-color: var(--primary-color-light);
cursor: pointer;
}
.data-table td {
color: var(--text-color);
font-size: 0.95rem;
}
/* Lists (e.g., for items or dynamic content) */
.custom-list {
list-style: none;
padding: 0;
margin: 1.5rem 0;
background-color: var(--secondary-color);
border-radius: 8px;
overflow: hidden;
}
.custom-list-item {
padding: 1rem 1.2rem;
border-bottom: 1px solid var(--neutral-dark);
color: var(--text-color);
transition: background-color 0.2s ease;
}
.custom-list-item:last-child {
border-bottom: none;
}
.custom-list-item:hover {
background-color: var(--primary-color-light);
cursor: pointer;
}
.custom-list-item-header {
font-weight: 600;
color: var(--heading-color);
margin-bottom: 0.5rem;
}
.custom-list-item-meta {
font-size: 0.85rem;
color: var(--neutral-medium);
}
/* Custom Visual Elements (e.g., panels, containers) */
.panel {
background-color: var(--secondary-color);
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
margin-bottom: 1.5rem;
}
.panel-header {
font-size: 1.5rem;
color: var(--heading-color);
margin-bottom: 1rem;
border-bottom: 1px solid var(--neutral-dark);
padding-bottom: 0.5rem;
}
.panel-content {
color: var(--text-color);
line-height: 1.6;
}
.flex-container {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.flex-item {
flex: 1;
min-width: 250px;
}
/* Action Buttons within forms/lists */
.button-action {
background-color: var(--success-color);
padding: 0.5rem 1rem;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.85rem;
color: var(--button-text);
transition: background-color 0.2s ease;
}
.button-action:hover {
background-color: var(--success-color-hover);
}
.button-danger {
background-color: var(--danger-color);
}
.button-danger:hover {
background-color: var(--danger-color-hover);
}
.button-group {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
/* /*
------------------------------------------------- -------------------------------------------------
*/ */

View File

@ -79,16 +79,16 @@ FieldName (text) = Some Value # This is an inline comment.
The .szf format supports a variety of field types to handle different kinds of data. These types are declared in character_template and dataset files. The .szf format supports a variety of field types to handle different kinds of data. These types are declared in character_template and dataset files.
| Type | Description | Example Value | | Type | Description | Example Value |
| :---- | :---- | :---- | |:---------------------|:----------------------------------------------------------------|:---------------------------------------------|
| text | A single line of text. | Elara the Brave | | text | A single line of text. | Elara the Brave |
| text-field | A multi-line block of text. | A long sword forged by...\nIt glows faintly. | | text-field | A multi-line block of text. | A long sword forged by...\nIt glows faintly. |
| number | An integer or floating-point number. | 16 or 3.5 | | number | An integer or floating-point number. | 16 or 3.5 |
| bool | A boolean value. | true or false | | bool | A boolean value. | true or false |
| calculated | A value derived from a formula. | (Strength - 10) / 2 | | calculated | A value derived from a formula. | (Strength - 10) / 2 |
| system | A special field that controls application behavior. | items | | system | A special field that controls application behavior. | |
| entry-reference | A reference to a single entry from a dataset. | ClassData or ItemData.Longsword | | entry-reference | A reference to a single entry from a dataset. | ClassData or ItemData.Longsword |
| entry-reference-list | A comma-separated list of references to entries from a dataset. | CoreFeats.PowerAttack, CoreFeats.Cleave | | entry-reference-list | A comma-separated list of references to entries from a dataset. | CoreFeats.PowerAttack, CoreFeats.Cleave |
## **4. File Type Specifications** ## **4. File Type Specifications**