Vibe json data export
This commit is contained in:
@@ -0,0 +1,123 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260929-233614-1hed
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Add a "Download JSON" button to the interactive Galaxy Map in `ZeroSpace.Web` that exports all data powering the galaxy map into a structured, downloadable JSON file (`galaxy-map-data.json`). The exported data will include all planetary systems with their full metadata and Markdown bodies, system masteries with their bonuses and notes, and planetary mastery items.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **In Scope**:
|
||||||
|
- A client-side browser file download utility via Blazor `IJSRuntime`.
|
||||||
|
- Aggregation and serialization of `StarSystems.All` (including all markdown notes in `Body`), `SystemMasteries.All`, and `PlanetaryMasteryItems.All`.
|
||||||
|
- A "Download JSON" button styled with the project's Sci-Fi theme in `ZeroSpace.Web/Pages/Galaxy.razor`.
|
||||||
|
- **Out of Scope**:
|
||||||
|
- Backend API changes (the export is generated client-side from the static/in-memory data models already present in Blazor WebAssembly).
|
||||||
|
- Editing or mutating star system data.
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
- **As a player / community developer**, I want to download all galaxy map data (including star systems, hyperlane connections, occupation buffs, masteries, and markdown notes) in JSON format from the Galaxy Map page so that I can analyze the data offline, create custom tooling, or reference game lore.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
1. **Download Action**: Clicking the "Download JSON" button generates and initiates a browser download of `galaxy-map-data.json`.
|
||||||
|
2. **Star System Data & Markdown**: The JSON output must contain all star systems from `StarSystems.All` including ID, name, category, owner, yield flags, occupation buff, system mastery, connected systems, coordinates, and full markdown text (`Body`).
|
||||||
|
3. **Mastery Data**: The JSON output must contain all system masteries from `SystemMasteries.All` (levels, available systems, stats, and markdown body) and planetary mastery items from `PlanetaryMasteryItems.All`.
|
||||||
|
4. **JSON Formatting**: The output JSON must be well-formatted (indented, camelCase property naming, string representations for enums).
|
||||||
|
5. **UI Integration**: The button must be positioned intuitively in the Galaxy header / control bar matching the existing glassmorphic Sci-Fi styling.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- **Performance**: Export generation and download initiation should take < 50ms without blocking UI interactions.
|
||||||
|
- **Offline / WebAssembly Compatibility**: The export must function entirely within the client-side Blazor WebAssembly runtime without requiring server endpoints.
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- `ZeroSpace.Web/Pages/Galaxy.razor`: The interactive galaxy map page displaying star systems, hyperlanes, and faction occupation details using `StarSystems.All` and `SystemMasteries.All`.
|
||||||
|
- `ZeroSpace.Model/StarSystemData.cs` & `StarSystems.g.cs`: Contains `StarSystemData` records where `Body` contains the planetary system Markdown notes.
|
||||||
|
- `ZeroSpace.Model/SystemMasteryData.cs` & `SystemMasteries.g.cs`: Contains `SystemMasteryData` records with mastery levels, system associations, stats, and markdown `Body`.
|
||||||
|
- `ZeroSpace.Model/PlanetaryMasteryItemData.cs` & `PlanetaryMasteryItems.g.cs`: Contains `PlanetaryMasteryItemData` records for mastery items.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
1. **Client-Side Export**: Use Blazor WebAssembly's `IJSRuntime` with a lightweight browser Blob URL helper to trigger file download instantly in memory without backend roundtrips.
|
||||||
|
2. **Unified Data Structure**: Group data under top-level properties (`systems`, `masteries`, `planetaryMasteryItems`, `exportedAt`) for easy consumption by external tools.
|
||||||
|
3. **Formatting**: Use `System.Text.Json` with `JsonSerializerOptions` configured for `WriteIndented = true`, `PropertyNamingPolicy = JsonNamingPolicy.CamelCase`, and `JsonStringEnumConverter`.
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
1. **File Download Helper**:
|
||||||
|
- Add `ZeroSpace.Web/wwwroot/js/fileDownload.js` containing `downloadFileFromText(fileName, contentType, textContent)`.
|
||||||
|
- Include script reference in `ZeroSpace.Web/wwwroot/index.html`.
|
||||||
|
2. **Export Model**:
|
||||||
|
- Define `GalaxyMapExportData` record containing `IReadOnlyList<StarSystemData> Systems`, `IReadOnlyList<SystemMasteryData> Masteries`, `IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems`, and `DateTimeOffset ExportedAt`.
|
||||||
|
3. **Galaxy Page Update**:
|
||||||
|
- Add a "Download JSON" button to the header action bar in `Galaxy.razor`.
|
||||||
|
- Add `DownloadJsonAsync()` method in `@code` section that builds `GalaxyMapExportData`, serializes to JSON, and calls the JS helper.
|
||||||
|
|
||||||
|
### Data Models / Contracts
|
||||||
|
```csharp
|
||||||
|
public sealed record GalaxyMapExportData(
|
||||||
|
DateTimeOffset ExportedAt,
|
||||||
|
int TotalSystems,
|
||||||
|
int TotalMasteries,
|
||||||
|
IReadOnlyList<StarSystemData> Systems,
|
||||||
|
IReadOnlyList<SystemMasteryData> Masteries,
|
||||||
|
IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture Diagram
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
User[User clicks Download JSON] --> Razor[Galaxy.razor]
|
||||||
|
Razor --> Aggregator[Aggregate StarSystems, Masteries, Items]
|
||||||
|
Aggregator --> Serializer[System.Text.Json Serializer]
|
||||||
|
Serializer --> JSInterop[IJSRuntime downloadFileFromText]
|
||||||
|
JSInterop --> Browser[Browser saves galaxy-map-data.json]
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
- `ZeroSpace.Web/wwwroot/js/fileDownload.js` *(new)*
|
||||||
|
- `ZeroSpace.Web/wwwroot/index.html` *(modified to include script tag)*
|
||||||
|
- `ZeroSpace.Web/Models/GalaxyMapExportData.cs` *(new)*
|
||||||
|
- `ZeroSpace.Web/Pages/Galaxy.razor` *(modified to add download button and handler)*
|
||||||
|
- `ZeroSpace.Web/Pages/Galaxy.razor.css` *(optional style tweaks for button layout)*
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
- **Large Payload / Memory**: The total size of all systems and markdown is < 500 KB, which fits easily in browser memory for Blob generation.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
- Verify solution builds with 0 errors via `build_solution_start` and `build_solution_state`.
|
||||||
|
- Verify the serialized JSON structure against required fields (star system metadata, markdown `Body`, masteries, levels, stats).
|
||||||
|
- Verify the button UI placement, styling, and JSInterop invocation logic.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
1. **Download Button Interaction**: User clicks the "Download JSON" button on `/galaxy`.
|
||||||
|
- Expected: `downloadFileFromText` JSInterop is called with `galaxy-map-data.json`, `application/json`, and the formatted JSON string.
|
||||||
|
2. **JSON Content Completeness**:
|
||||||
|
- Every star system from `StarSystems.All` is present in the `systems` array.
|
||||||
|
- The markdown `body` field of each star system is included.
|
||||||
|
- Every mastery from `SystemMasteries.All` is present in the `masteries` array with levels and stats.
|
||||||
|
- Planetary mastery items from `PlanetaryMasteryItems.All` are included.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- Systems with empty vs populated markdown `Body` serialize properly as strings (`""` or markdown text).
|
||||||
|
- Enum values (`StarSystemCategory`) serialize cleanly as strings (e.g. `"homeworldSystem"`, `"coreSystem"`, `"sideSystem"` or PascalCase).
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### ✓ Step 1: Implement file download JSInterop helper and export serialization model
|
||||||
|
Create the file download JSInterop utility and the galaxy map data export serialization contract.
|
||||||
|
|
||||||
|
- Add a JavaScript file download helper (`ZeroSpace.Web/wwwroot/js/fileDownload.js` or equivalent) that accepts filename, content type, and content/bytes, creating an object URL blob and triggering a download anchor click.
|
||||||
|
- Reference the script in `ZeroSpace.Web/wwwroot/index.html`.
|
||||||
|
- Define an export container record (e.g., `GalaxyMapExportData`) in `ZeroSpace.Web` / `ZeroSpace.Model` that aggregates star systems (with full metadata and markdown `Body`), system masteries (with levels, stats, available systems, and `Body`), and planetary mastery items.
|
||||||
|
- Implement a JSON serialization helper/service configured with `JsonSerializerOptions` (`WriteIndented = true`, camelCase naming, `JsonStringEnumConverter`) for clean formatting.
|
||||||
|
|
||||||
|
### ✓ Step 2: Integrate Download JSON button and handler into the Galaxy Map page
|
||||||
|
Add the Download JSON button to the Galaxy Map page and wire up the export trigger.
|
||||||
|
|
||||||
|
- Add the "Download JSON" button in the top action bar of `ZeroSpace.Web/Pages/Galaxy.razor` next to the map legend/stats controls.
|
||||||
|
- Apply consistent Sci-Fi design system styling (`btn-scifi-outline` with appropriate icon/label) in `Galaxy.razor` and `Galaxy.razor.css`.
|
||||||
|
- Inject `IJSRuntime` in `Galaxy.razor` and implement the `DownloadGalaxyJsonAsync` event handler to generate the export payload and trigger the client-side download named `galaxy-map-data.json`.
|
||||||
|
- Build the solution and verify clean compilation without errors or warnings.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace ZeroSpace.Model;
|
||||||
|
|
||||||
|
public sealed record GalaxyMapExportData(
|
||||||
|
DateTimeOffset ExportedAt,
|
||||||
|
int TotalSystems,
|
||||||
|
int TotalMasteries,
|
||||||
|
IReadOnlyList<StarSystemData> Systems,
|
||||||
|
IReadOnlyList<SystemMasteryData> Masteries,
|
||||||
|
IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems)
|
||||||
|
{
|
||||||
|
public static GalaxyMapExportData CreateDefault() =>
|
||||||
|
new(
|
||||||
|
DateTimeOffset.UtcNow,
|
||||||
|
StarSystems.All.Count,
|
||||||
|
SystemMasteries.All.Count,
|
||||||
|
StarSystems.All,
|
||||||
|
SystemMasteries.All,
|
||||||
|
ZeroSpace.Model.PlanetaryMasteryItems.All);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@using System.Text.RegularExpressions
|
@using System.Text.RegularExpressions
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime Js
|
||||||
|
@inject IGalaxyMapExportSerializer ExportSerializer
|
||||||
|
|
||||||
<PageTitle>Galaxy Map</PageTitle>
|
<PageTitle>Galaxy Map</PageTitle>
|
||||||
|
|
||||||
@@ -9,7 +11,8 @@
|
|||||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
|
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
|
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
|
||||||
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and hyperlane routes.</p>
|
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and
|
||||||
|
hyperlane routes.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||||
<div class="map-legend d-flex flex-wrap align-items-center gap-3 px-3 py-2 rounded">
|
<div class="map-legend d-flex flex-wrap align-items-center gap-3 px-3 py-2 rounded">
|
||||||
@@ -34,6 +37,19 @@
|
|||||||
<span class="mx-1">•</span>
|
<span class="mx-1">•</span>
|
||||||
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
<span><strong>@_connections.Count</strong> Hyperlanes</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-sm btn-scifi-outline d-inline-flex align-items-center gap-2 px-3 py-2"
|
||||||
|
@onclick="DownloadGalaxyJsonAsync"
|
||||||
|
disabled="@_isExporting"
|
||||||
|
title="Download galaxy map data as JSON">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"
|
||||||
|
aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path
|
||||||
|
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
<span>@(_isExporting ? "Exporting..." : "Download JSON")</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +57,8 @@
|
|||||||
<div class="filter-bar zs-glass-card p-3 mb-3 d-flex flex-wrap align-items-center gap-3">
|
<div class="filter-bar zs-glass-card p-3 mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||||
<div class="d-flex align-items-center gap-2 filter-item">
|
<div class="d-flex align-items-center gap-2 filter-item">
|
||||||
<label class="filter-label text-dim small fw-semibold text-uppercase">System Type:</label>
|
<label class="filter-label text-dim small fw-semibold text-uppercase">System Type:</label>
|
||||||
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory" @bind:after="ApplyFilters">
|
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory"
|
||||||
|
@bind:after="ApplyFilters">
|
||||||
<option value="">All Types</option>
|
<option value="">All Types</option>
|
||||||
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
|
||||||
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
<option value="@StarSystemCategory.CoreSystem">Core System</option>
|
||||||
@@ -69,7 +86,7 @@
|
|||||||
<option value="Influence">Higher Influence</option>
|
<option value="Influence">Higher Influence</option>
|
||||||
<option value="Credits">Higher Credits</option>
|
<option value="Credits">Higher Credits</option>
|
||||||
<option value="Energy">Higher Energy</option>
|
<option value="Energy">Higher Energy</option>
|
||||||
<option value="Items">Higher Items</option>
|
<option value="Items">Higher Items (In current item, items only attainable in shops)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,263 +100,272 @@
|
|||||||
|
|
||||||
<div class="galaxy-layout">
|
<div class="galaxy-layout">
|
||||||
<div class="galaxy-map-container" @onmouseleave="OnContainerMouseLeave">
|
<div class="galaxy-map-container" @onmouseleave="OnContainerMouseLeave">
|
||||||
<svg viewBox="0 0 1000 1000" class="galaxy-svg" preserveAspectRatio="xMidYMid meet"
|
<svg viewBox="0 0 1000 1000" class="galaxy-svg" preserveAspectRatio="xMidYMid meet"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<!-- Background Grid Pattern -->
|
<!-- Background Grid Pattern -->
|
||||||
<pattern id="space-grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
<pattern id="space-grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||||
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(56, 189, 248, 0.04)" stroke-width="1"/>
|
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(56, 189, 248, 0.04)" stroke-width="1"/>
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<!-- Center Core Glow -->
|
<!-- Center Core Glow -->
|
||||||
<radialGradient id="galaxy-core-glow" cx="50%" cy="50%" r="50%">
|
<radialGradient id="galaxy-core-glow" cx="50%" cy="50%" r="50%">
|
||||||
<stop offset="0%" stop-color="rgba(56, 189, 248, 0.08)"/>
|
<stop offset="0%" stop-color="rgba(56, 189, 248, 0.08)"/>
|
||||||
<stop offset="60%" stop-color="rgba(16, 185, 129, 0.03)"/>
|
<stop offset="60%" stop-color="rgba(16, 185, 129, 0.03)"/>
|
||||||
<stop offset="100%" stop-color="rgba(13, 17, 23, 0)"/>
|
<stop offset="100%" stop-color="rgba(13, 17, 23, 0)"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
|
|
||||||
<!-- Glow filters for Owners / Factions -->
|
<!-- Glow filters for Owners / Factions -->
|
||||||
<filter id="glow-eden" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="glow-eden" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||||
<feMerge>
|
<feMerge>
|
||||||
<feMergeNode in="blur"/>
|
<feMergeNode in="blur"/>
|
||||||
<feMergeNode in="SourceGraphic"/>
|
<feMergeNode in="SourceGraphic"/>
|
||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</filter>
|
||||||
<filter id="glow-keeper" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="glow-keeper" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||||
<feMerge>
|
<feMerge>
|
||||||
<feMergeNode in="blur"/>
|
<feMergeNode in="blur"/>
|
||||||
<feMergeNode in="SourceGraphic"/>
|
<feMergeNode in="SourceGraphic"/>
|
||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</filter>
|
||||||
<filter id="glow-obsidian" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="glow-obsidian" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
||||||
<feMerge>
|
<feMerge>
|
||||||
<feMergeNode in="blur"/>
|
<feMergeNode in="blur"/>
|
||||||
<feMergeNode in="SourceGraphic"/>
|
<feMergeNode in="SourceGraphic"/>
|
||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- Map Background & Grid -->
|
<!-- Map Background & Grid -->
|
||||||
<rect width="1000" height="1000" class="map-bg"/>
|
<rect width="1000" height="1000" class="map-bg"/>
|
||||||
<rect width="1000" height="1000" fill="url(#space-grid)"/>
|
<rect width="1000" height="1000" fill="url(#space-grid)"/>
|
||||||
<rect width="1000" height="1000" fill="url(#galaxy-core-glow)"/>
|
<rect width="1000" height="1000" fill="url(#galaxy-core-glow)"/>
|
||||||
|
|
||||||
<!-- Connection Lines (Hyperlanes) -->
|
<!-- Connection Lines (Hyperlanes) -->
|
||||||
<g class="hyperlanes">
|
<g class="hyperlanes">
|
||||||
@foreach (var edge in _connections)
|
@foreach (var edge in _connections)
|
||||||
{
|
{
|
||||||
var isFromMatch = _matchingSystemIds.Contains(edge.From.Id);
|
var isFromMatch = _matchingSystemIds.Contains(edge.From.Id);
|
||||||
var isToMatch = _matchingSystemIds.Contains(edge.To.Id);
|
var isToMatch = _matchingSystemIds.Contains(edge.To.Id);
|
||||||
var isFilterDimmed = HasActiveFilters && (!isFromMatch || !isToMatch);
|
var isFilterDimmed = HasActiveFilters && (!isFromMatch || !isToMatch);
|
||||||
|
|
||||||
var isHoverHighlighted = _hoveredSystem is not null &&
|
var isHoverHighlighted = _hoveredSystem is not null &&
|
||||||
(string.Equals(edge.From.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase) ||
|
(string.Equals(edge.From.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(edge.To.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase));
|
string.Equals(edge.To.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase));
|
||||||
var isHoverDimmed = _hoveredSystem is not null && !isHoverHighlighted;
|
var isHoverDimmed = _hoveredSystem is not null && !isHoverHighlighted;
|
||||||
|
|
||||||
var lineClass = isHoverHighlighted ? "hyperlane-line hyperlane-highlighted" :
|
var lineClass = isHoverHighlighted ? "hyperlane-line hyperlane-highlighted" :
|
||||||
(isHoverDimmed || isFilterDimmed) ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line";
|
(isHoverDimmed || isFilterDimmed) ? "hyperlane-line hyperlane-dimmed" : "hyperlane-line";
|
||||||
<line x1="@SvgNumber(edge.From.X)"
|
<line x1="@SvgNumber(edge.From.X)"
|
||||||
y1="@SvgNumber(edge.From.Y)"
|
y1="@SvgNumber(edge.From.Y)"
|
||||||
x2="@SvgNumber(edge.To.X)"
|
x2="@SvgNumber(edge.To.X)"
|
||||||
y2="@SvgNumber(edge.To.Y)"
|
y2="@SvgNumber(edge.To.Y)"
|
||||||
class="@lineClass"/>
|
class="@lineClass"/>
|
||||||
}
|
}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Planetary Nodes -->
|
<!-- Planetary Nodes -->
|
||||||
<g class="planetary-nodes">
|
<g class="planetary-nodes">
|
||||||
@foreach (var system in _systems)
|
@foreach (var system in _systems)
|
||||||
{
|
{
|
||||||
var ownerClass = GetOwnerCssClass(system.Owner);
|
var ownerClass = GetOwnerCssClass(system.Owner);
|
||||||
var radius = GetNodeRadius(system.SystemCategory);
|
var radius = GetNodeRadius(system.SystemCategory);
|
||||||
var isMatch = _matchingSystemIds.Contains(system.Id);
|
var isMatch = _matchingSystemIds.Contains(system.Id);
|
||||||
var isHovered = _hoveredSystem is not null && string.Equals(system.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase);
|
var isHovered = _hoveredSystem is not null && string.Equals(system.Id, _hoveredSystem.Id, StringComparison.OrdinalIgnoreCase);
|
||||||
var isNeighbor = _hoveredConnectedIds.Contains(system.Id);
|
var isNeighbor = _hoveredConnectedIds.Contains(system.Id);
|
||||||
var isDimmed = (_hoveredSystem is not null && !isHovered && !isNeighbor) || (HasActiveFilters && !isMatch);
|
var isDimmed = (_hoveredSystem is not null && !isHovered && !isNeighbor) || (HasActiveFilters && !isMatch);
|
||||||
var stateClass = isHovered ? "is-hovered" :
|
var stateClass = isHovered ? "is-hovered" :
|
||||||
isNeighbor ? "is-neighbor" :
|
isNeighbor ? "is-neighbor" :
|
||||||
isDimmed ? "is-dimmed" :
|
isDimmed ? "is-dimmed" :
|
||||||
(HasActiveFilters && isMatch ? "is-filter-matched" : "");
|
(HasActiveFilters && isMatch ? "is-filter-matched" : "");
|
||||||
|
|
||||||
<g class="node-group @ownerClass @stateClass"
|
<g class="node-group @ownerClass @stateClass"
|
||||||
transform="translate(@SvgNumber(system.X), @SvgNumber(system.Y))"
|
transform="translate(@SvgNumber(system.X), @SvgNumber(system.Y))"
|
||||||
@onclick="() => OnNodeClick(system.Id)"
|
@onclick="() => OnNodeClick(system.Id)"
|
||||||
@onmouseenter="() => OnNodeMouseEnter(system)"
|
@onmouseenter="() => OnNodeMouseEnter(system)"
|
||||||
@onmouseleave="() => OnNodeMouseLeave(system)"
|
@onmouseleave="() => OnNodeMouseLeave(system)"
|
||||||
@onfocus="() => OnNodeFocus(system)"
|
@onfocus="() => OnNodeFocus(system)"
|
||||||
@onblur="OnNodeBlur"
|
@onblur="OnNodeBlur"
|
||||||
@onkeydown="e => OnNodeKeyDown(e, system.Id)"
|
@onkeydown="e => OnNodeKeyDown(e, system.Id)"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="@system.Name (@system.SystemCategory.ToDisplayName(), @system.Owner)">
|
aria-label="@system.Name (@system.SystemCategory.ToDisplayName(), @system.Owner)">
|
||||||
|
|
||||||
<!-- Outer aura/glow circle -->
|
<!-- Outer aura/glow circle -->
|
||||||
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
|
<circle r="@SvgNumber(radius + 4)" class="node-aura"/>
|
||||||
|
|
||||||
<!-- Main Node Circle -->
|
<!-- Main Node Circle -->
|
||||||
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
<circle r="@SvgNumber(radius)" class="node-circle"/>
|
||||||
|
|
||||||
<!-- Center core dot for homeworlds and core systems -->
|
<!-- Center core dot for homeworlds and core systems -->
|
||||||
@if (system.SystemCategory != StarSystemCategory.SideSystem)
|
@if (system.SystemCategory != StarSystemCategory.SideSystem)
|
||||||
{
|
{
|
||||||
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
<circle r="@SvgNumber(radius * 0.4)" class="node-core-dot"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- System Name Label -->
|
<!-- System Name Label -->
|
||||||
<text y="@SvgNumber(radius + 10)" class="node-label" text-anchor="middle">@system.Name</text>
|
<text y="@SvgNumber(radius + 10)" class="node-label"
|
||||||
</g>
|
text-anchor="middle">@system.Name</text>
|
||||||
}
|
</g>
|
||||||
</g>
|
}
|
||||||
</svg>
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- Floating Glassmorphism Hover Card -->
|
<!-- Floating Glassmorphism Hover Card -->
|
||||||
@if (_hoveredSystem is not null)
|
@if (_hoveredSystem is not null)
|
||||||
{
|
{
|
||||||
var ownerClass = GetOwnerCssClass(_hoveredSystem.Owner);
|
var ownerClass = GetOwnerCssClass(_hoveredSystem.Owner);
|
||||||
var higherYields = GetHigherYields(_hoveredSystem);
|
var higherYields = GetHigherYields(_hoveredSystem);
|
||||||
<div class="planet-hover-card @ownerClass"
|
<div class="planet-hover-card @ownerClass"
|
||||||
style="@GetTooltipStyle(_hoveredSystem)"
|
style="@GetTooltipStyle(_hoveredSystem)"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
aria-live="polite">
|
aria-live="polite">
|
||||||
<div class="card-header-row d-flex align-items-center justify-content-between mb-2">
|
<div class="card-header-row d-flex align-items-center justify-content-between mb-2">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span class="system-owner-indicator"></span>
|
<span class="system-owner-indicator"></span>
|
||||||
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
<span
|
||||||
</div>
|
class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
|
||||||
|
|
||||||
<div class="card-details-grid">
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">Faction Owner</span>
|
|
||||||
<span class="badge @OwnerBadgeClass(_hoveredSystem.Owner) mt-1">@_hoveredSystem.Owner</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="card-details-grid">
|
||||||
<span class="detail-label">Occupation Buff</span>
|
<div class="detail-row">
|
||||||
<span class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
<span class="detail-label">Faction Owner</span>
|
||||||
|
<span class="badge @OwnerBadgeClass(_hoveredSystem.Owner) mt-1">@_hoveredSystem.Owner</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Occupation Buff</span>
|
||||||
|
<span
|
||||||
|
class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
|
||||||
@(_hoveredSystem.OccupationBuff ?? "—")
|
@(_hoveredSystem.OccupationBuff ?? "—")
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">System Mastery</span>
|
<span class="detail-label">System Mastery</span>
|
||||||
<span class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
<span
|
||||||
|
class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
|
||||||
@(_hoveredSystem.SystemMastery ?? "—")
|
@(_hoveredSystem.SystemMastery ?? "—")
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">Yield Bonuses</span>
|
<span class="detail-label">Yield Bonuses</span>
|
||||||
<div class="yield-badges d-flex flex-wrap gap-1 mt-1">
|
<div class="yield-badges d-flex flex-wrap gap-1 mt-1">
|
||||||
@if (higherYields.Count == 0)
|
@if (higherYields.Count == 0)
|
||||||
{
|
|
||||||
<span class="yield-badge yield-standard">Standard</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@foreach (var y in higherYields)
|
|
||||||
{
|
{
|
||||||
<span class="yield-badge yield-@y.ToLowerInvariant()">+@y</span>
|
<span class="yield-badge yield-standard">Standard</span>
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
@foreach (var y in higherYields)
|
||||||
|
{
|
||||||
|
<span class="yield-badge yield-@y.ToLowerInvariant()">+@y</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<span class="detail-label">Connected Systems</span>
|
||||||
|
<span
|
||||||
|
class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
||||||
|
</div>
|
||||||
|
<div class="connected-systems-list text-secondary small mt-1">
|
||||||
|
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="card-footer-hint mt-2 pt-2 border-top border-secondary border-opacity-25 text-center">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<span class="hint-text small">Click system to view full details →</span>
|
||||||
<span class="detail-label">Connected Systems</span>
|
|
||||||
<span class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
|
|
||||||
</div>
|
|
||||||
<div class="connected-systems-list text-secondary small mt-1">
|
|
||||||
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="card-footer-hint mt-2 pt-2 border-top border-secondary border-opacity-25 text-center">
|
|
||||||
<span class="hint-text small">Click system to view full details →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")" aria-label="Faction occupational bonuses">
|
|
||||||
<div class="drawer-header d-flex align-items-center justify-content-between gap-2">
|
|
||||||
<div>
|
|
||||||
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
|
||||||
<p class="drawer-subtitle text-secondary mb-0">Occupation buffs by faction</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-scifi-outline drawer-toggle" @onclick="ToggleDrawer"
|
|
||||||
aria-expanded="@(!_drawerCollapsed)"
|
|
||||||
title="@(_drawerCollapsed ? "Expand bonus panel" : "Collapse bonus panel")">
|
|
||||||
@(_drawerCollapsed ? "»" : "«")
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!_drawerCollapsed)
|
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")"
|
||||||
{
|
aria-label="Faction occupational bonuses">
|
||||||
<div class="drawer-body">
|
<div class="drawer-header d-flex align-items-center justify-content-between gap-2">
|
||||||
<div class="faction-picker" role="radiogroup" aria-label="Select faction">
|
<div>
|
||||||
@foreach (var owner in StarSystemOwners.All)
|
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
|
||||||
{
|
<p class="drawer-subtitle text-secondary mb-0">Occupation buffs by faction</p>
|
||||||
var isActive = string.Equals(owner, _selectedFaction, StringComparison.Ordinal);
|
|
||||||
<button role="radio" aria-checked="@isActive"
|
|
||||||
class="faction-pick @(isActive ? "is-active " + GetOwnerCssClass(owner) : "")"
|
|
||||||
@onclick="() => SelectFaction(owner)">
|
|
||||||
<span class="legend-dot @FactionDotClass(owner)"></span>
|
|
||||||
<span class="faction-pick-name">@ShortFactionName(owner)</span>
|
|
||||||
<span class="faction-pick-count">@FactionSystemCount(owner)</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-sm btn-scifi-outline drawer-toggle" @onclick="ToggleDrawer"
|
||||||
|
aria-expanded="@(!_drawerCollapsed)"
|
||||||
|
title="@(_drawerCollapsed ? "Expand bonus panel" : "Collapse bonus panel")">
|
||||||
|
@(_drawerCollapsed ? "»" : "«")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="drawer-section">
|
@if (!_drawerCollapsed)
|
||||||
<h3 class="drawer-section-title">Occupational bonuses</h3>
|
{
|
||||||
<p class="drawer-summary text-secondary">
|
<div class="drawer-body">
|
||||||
<strong class="text-light">@FactionSystems.Count</strong> systems owned by
|
<div class="faction-picker" role="radiogroup" aria-label="Select faction">
|
||||||
<strong class="text-light">@ShortFactionName(_selectedFaction)</strong>
|
@foreach (var owner in StarSystemOwners.All)
|
||||||
</p>
|
{
|
||||||
@if (FactionBonuses.Count == 0)
|
var isActive = string.Equals(owner, _selectedFaction, StringComparison.Ordinal);
|
||||||
{
|
<button role="radio" aria-checked="@isActive"
|
||||||
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's systems.</p>
|
class="faction-pick @(isActive ? "is-active " + GetOwnerCssClass(owner) : "")"
|
||||||
}
|
@onclick="() => SelectFaction(owner)">
|
||||||
else
|
<span class="legend-dot @FactionDotClass(owner)"></span>
|
||||||
{
|
<span class="faction-pick-name">@ShortFactionName(owner)</span>
|
||||||
<ul class="bonus-list list-unstyled mb-0">
|
<span class="faction-pick-count">@FactionSystemCount(owner)</span>
|
||||||
@foreach (var bonus in FactionBonuses.OrderBy(b => b.Label))
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-section">
|
||||||
|
<h3 class="drawer-section-title">Occupational bonuses</h3>
|
||||||
|
<p class="drawer-summary text-secondary">
|
||||||
|
<strong class="text-light">@FactionSystems.Count</strong> systems owned by
|
||||||
|
<strong class="text-light">@ShortFactionName(_selectedFaction)</strong>
|
||||||
|
</p>
|
||||||
|
@if (FactionBonuses.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's
|
||||||
|
systems.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<ul class="bonus-list list-unstyled mb-0">
|
||||||
|
@foreach (var bonus in FactionBonuses.OrderBy(b => b.Label))
|
||||||
|
{
|
||||||
|
<li class="bonus-row">
|
||||||
|
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
||||||
|
<span class="bonus-label">@bonus.Label</span>
|
||||||
|
<span
|
||||||
|
class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-section">
|
||||||
|
<h3 class="drawer-section-title">Planets (@FactionSystems.Count)</h3>
|
||||||
|
<ul class="planet-list list-unstyled mb-0">
|
||||||
|
@foreach (var system in FactionSystems)
|
||||||
{
|
{
|
||||||
<li class="bonus-row">
|
<li>
|
||||||
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
|
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)"
|
||||||
<span class="bonus-label">@bonus.Label</span>
|
title="View @system.Name details">
|
||||||
<span class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
|
<span class="planet-name">@system.Name</span>
|
||||||
|
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="drawer-section">
|
</aside>
|
||||||
<h3 class="drawer-section-title">Planets (@FactionSystems.Count)</h3>
|
|
||||||
<ul class="planet-list list-unstyled mb-0">
|
|
||||||
@foreach (var system in FactionSystems)
|
|
||||||
{
|
|
||||||
<li>
|
|
||||||
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)" title="View @system.Name details">
|
|
||||||
<span class="planet-name">@system.Name</span>
|
|
||||||
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -362,6 +388,7 @@
|
|||||||
// Faction picker: one of the three factions is always selected.
|
// Faction picker: one of the three factions is always selected.
|
||||||
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
private string _selectedFaction = StarSystemOwners.EdenAccord;
|
||||||
private bool _drawerCollapsed;
|
private bool _drawerCollapsed;
|
||||||
|
private bool _isExporting;
|
||||||
|
|
||||||
private bool HasActiveFilters =>
|
private bool HasActiveFilters =>
|
||||||
_selectedCategory.HasValue ||
|
_selectedCategory.HasValue ||
|
||||||
@@ -386,6 +413,7 @@
|
|||||||
_matchingSystemIds.Add(system.Id);
|
_matchingSystemIds.Add(system.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_matchingCount = _matchingSystemIds.Count;
|
_matchingCount = _matchingSystemIds.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +462,23 @@
|
|||||||
ApplyFilters();
|
ApplyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task DownloadGalaxyJsonAsync()
|
||||||
|
{
|
||||||
|
if (_isExporting) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isExporting = true;
|
||||||
|
var exportData = GalaxyMapExportData.CreateDefault();
|
||||||
|
var json = ExportSerializer.Serialize(exportData);
|
||||||
|
await Js.InvokeVoidAsync("downloadFileFromText", "galaxy-map-data.json", "application/json", json);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isExporting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----- faction bonus drawer -----
|
// ----- faction bonus drawer -----
|
||||||
|
|
||||||
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.
|
|||||||
builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
||||||
{
|
{
|
||||||
var apiBaseUrl = builder.Configuration["services:apiservice:https:0"]
|
var apiBaseUrl = builder.Configuration["services:apiservice:https:0"]
|
||||||
?? builder.Configuration["services:apiservice:http:0"]
|
?? builder.Configuration["services:apiservice:http:0"]
|
||||||
?? builder.Configuration["ApiServiceUrl"]
|
?? builder.Configuration["ApiServiceUrl"]
|
||||||
?? builder.HostEnvironment.BaseAddress;
|
?? builder.HostEnvironment.BaseAddress;
|
||||||
|
|
||||||
if (!apiBaseUrl.EndsWith('/'))
|
if (!apiBaseUrl.EndsWith('/'))
|
||||||
{
|
{
|
||||||
@@ -24,4 +24,6 @@ builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
|
|||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IGalaxyMapExportSerializer, GalaxyMapExportSerializer>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public sealed class GalaxyMapExportSerializer : IGalaxyMapExportSerializer
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions DefaultOptions = CreateDefaultOptions();
|
||||||
|
|
||||||
|
public static JsonSerializerOptions CreateDefaultOptions()
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Serialize(GalaxyMapExportData data) => SerializeData(data);
|
||||||
|
|
||||||
|
public static string SerializeData(GalaxyMapExportData data, JsonSerializerOptions? options = null)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(data, options ?? DefaultOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ZeroSpace.Model;
|
||||||
|
|
||||||
|
namespace ZeroSpace.Web.Services;
|
||||||
|
|
||||||
|
public interface IGalaxyMapExportSerializer
|
||||||
|
{
|
||||||
|
string Serialize(GalaxyMapExportData data);
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@
|
|||||||
<a href="." class="reload">Reload</a>
|
<a href="." class="reload">Reload</a>
|
||||||
<span class="dismiss">🗙</span>
|
<span class="dismiss">🗙</span>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="js/fileDownload.js"></script>
|
||||||
|
|
||||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.downloadFileFromText = function (fileName, contentType, textContent) {
|
||||||
|
const blob = new Blob([textContent], { type: contentType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = fileName || 'download.json';
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user