Added more Admin QOL stuff. DW.

This commit is contained in:
sarah committed 2026-09-05 14:04:21 +02:00
1 parent ab0d858fef
commit 24842c65fb
12 files changed
+4236

No files matched your search

+53
View File
@@ -54,8 +54,61 @@ $PageSection = "Admin";
Build mutation recipes with a visual grid.
</span>
</a>
<a
class="ActionButton AdminEditorLink"
href="/Pages/AdminMassPlants.html"
>
<strong>Mass plant editor</strong>
<span>
Apply selected plant fields to several plants at once.
</span>
</a>
<a
class="ActionButton AdminEditorLink"
href="/Pages/AdminMassMutations.html"
>
<strong>Mass mutation editor</strong>
<span>
Apply selected mutation fields to several mutations at once.
</span>
</a>
</div>
<section
id="AdminOverview"
class="Panel AdminOverview"
hidden
>
<div class="PanelHeader">
<h2>Quick overview</h2>
</div>
<p
id="AdminOverviewMessage"
class="PageMessage"
aria-live="polite"
></p>
<div class="AdminOverviewGroups">
<section>
<h2>Plants</h2>
<div id="AdminOverviewMissingSprites"></div>
<div id="AdminOverviewNoObtain"></div>
</section>
<section>
<h2>Mutations</h2>
<div id="AdminOverviewNoDescription"></div>
<div id="AdminOverviewNoHint"></div>
</section>
</div>
</section>
</div>
</main>
<script src="/Scripts/Garden/PlantImages.js"></script>
<script src="/Scripts/Garden/AdminOverview.js"></script>
</body>
</html>
+269
View File
@@ -0,0 +1,269 @@
<?php
$PageTitle = "Admin mass mutations";
$PageSection = "Admin";
?>
<!DOCTYPE html>
<html lang="en" data-palette="CatppuccinMocha">
<?php require __DIR__ . "/Bits/Head.php"; ?>
<body>
<?php require __DIR__ . "/Bits/Navbar.php"; ?>
<main class="MainContainer AdminMutationMain">
<h1>Mass mutation editor</h1>
<p>
Mark the mutations you want to edit, enable the fields to
change, then save. Fields left disabled stay untouched.
</p>
<p
id="AdminAccessMessage"
class="PageMessage"
aria-live="polite"
>
Checking admin access...
</p>
<div id="AdminContent" hidden>
<section class="Panel AdminMassSelectionPanel">
<div class="PanelHeader">
<h2>Mutations</h2>
</div>
<div class="AdminMassSelectionBody">
<div
id="AdminMassMutationSelection"
class="AdminPlantChecklist AdminMassChecklist"
></div>
<div class="AdminEditorActions">
<button
id="AdminMassMutationSelectAll"
class="ActionButton AdminInlineButton"
type="button"
>
Select all
</button>
<button
id="AdminMassMutationClear"
class="ActionButton AdminInlineButton"
type="button"
>
Clear selection
</button>
</div>
</div>
</section>
<form
id="AdminMassMutationForm"
class="Panel AdminEditorForm AdminMassEditorForm"
>
<div class="PanelHeader PanelHeaderInset">
<h2>Fields</h2>
</div>
<div class="AdminEditorGrid">
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Name">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Name</span>
<input id="AdminMassMutationName" type="text" maxlength="128">
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Description">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Description</span>
<textarea id="AdminMassMutationDescription" rows="4"></textarea>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Hint">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Hint</span>
<textarea id="AdminMassMutationHint" rows="3"></textarea>
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Priority">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Priority</span>
<input id="AdminMassMutationPriority" type="number" step="1" value="0">
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Chance">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Chance (%)</span>
<input id="AdminMassMutationChance" type="number" min="0" max="100" step="0.01" value="100">
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Cooldown">
<span>Apply</span>
</label>
<div class="AdminEditorField">
<span>Cooldown</span>
<div class="AdminDurationInputs">
<label>
<span>Hours</span>
<input id="AdminMassMutationCooldownHours" type="number" min="0" step="1" value="0">
</label>
<label>
<span>Minutes</span>
<input id="AdminMassMutationCooldownMinutes" type="number" min="0" max="59" step="1" value="0">
</label>
<label>
<span>Seconds</span>
<input id="AdminMassMutationCooldownSeconds" type="number" min="0" max="59" step="1" value="0">
</label>
</div>
</div>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Rotation">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Rotation</span>
<select id="AdminMassMutationRotation">
<option value="None">None</option>
<option value="Any">Any</option>
</select>
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="AllowImmature">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Allow immature plants</span>
<select id="AdminMassMutationAllowImmature">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Pattern">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Pattern JSON</span>
<textarea id="AdminMassMutationPattern" rows="7" spellcheck="false">[["Any"]]</textarea>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Success">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Success result JSON</span>
<textarea id="AdminMassMutationSuccess" rows="7" spellcheck="false">[["Keep"]]</textarea>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Failure">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Failure result JSON</span>
<textarea id="AdminMassMutationFailure" rows="7" spellcheck="false">"Keep"</textarea>
<small>Use "Keep", "Clear", or a result matrix.</small>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="PlantsUsed">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Plants used</span>
<input id="AdminMassMutationPlantsUsed" type="text" placeholder="RedRose, BlueRose">
<small>Comma-separated Plant Keys.</small>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="PlantsCreated">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Plants created</span>
<input id="AdminMassMutationPlantsCreated" type="text" placeholder="PurpleRose">
<small>Comma-separated Plant Keys.</small>
</label>
</div>
</div>
<div class="AdminEditorActions">
<button
id="AdminMassMutationSaveButton"
class="ActionButton AdminSaveButton"
type="submit"
>
Save marked mutations
</button>
<a
class="ActionButton AdminInlineButton AdminEditorBackLink"
href="/Pages/Admin.html"
>
Back to Admin
</a>
</div>
</form>
<p
id="AdminMassMutationMessage"
class="PageMessage"
aria-live="polite"
></p>
</div>
</main>
<script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/AdminValidation.js"></script>
<script src="/Scripts/Garden/AdminMassMutationEditor.js"></script>
</body>
</html>
+218
View File
@@ -0,0 +1,218 @@
<?php
$PageTitle = "Admin mass plants";
$PageSection = "Admin";
?>
<!DOCTYPE html>
<html lang="en" data-palette="CatppuccinMocha">
<?php require __DIR__ . "/Bits/Head.php"; ?>
<body>
<?php require __DIR__ . "/Bits/Navbar.php"; ?>
<main class="MainContainer">
<h1>Mass plant editor</h1>
<p>
Mark the plants you want to edit, enable the fields to
change, then save. Fields left disabled stay untouched.
</p>
<p
id="AdminAccessMessage"
class="PageMessage"
aria-live="polite"
>
Checking admin access...
</p>
<div id="AdminContent" hidden>
<section class="Panel AdminMassSelectionPanel">
<div class="PanelHeader">
<h2>Plants</h2>
</div>
<div class="AdminMassSelectionBody">
<div
id="AdminMassPlantSelection"
class="AdminPlantChecklist AdminMassChecklist"
></div>
<div class="AdminEditorActions">
<button
id="AdminMassPlantSelectAll"
class="ActionButton AdminInlineButton"
type="button"
>
Select all
</button>
<button
id="AdminMassPlantClear"
class="ActionButton AdminInlineButton"
type="button"
>
Clear selection
</button>
</div>
</div>
</section>
<form
id="AdminMassPlantForm"
class="Panel AdminEditorForm AdminMassEditorForm"
>
<div class="PanelHeader PanelHeaderInset">
<h2>Fields</h2>
</div>
<div class="AdminEditorGrid">
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Name">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Name</span>
<input id="AdminMassPlantName" type="text" maxlength="64">
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Description">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Description</span>
<textarea id="AdminMassPlantDescription" rows="4"></textarea>
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Tags">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Tags</span>
<input id="AdminMassPlantTags" type="text" placeholder="Rose, Flower, Red">
<small>Comma-separated.</small>
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="GrowthTime">
<span>Apply</span>
</label>
<div class="AdminEditorField">
<span>Growth time</span>
<div class="AdminDurationInputs">
<label>
<span>Hours</span>
<input id="AdminMassPlantGrowthHours" type="number" min="0" step="1" value="1">
</label>
<label>
<span>Minutes</span>
<input id="AdminMassPlantGrowthMinutes" type="number" min="0" max="59" step="1" value="0">
</label>
<label>
<span>Seconds</span>
<input id="AdminMassPlantGrowthSeconds" type="number" min="0" max="59" step="1" value="0">
</label>
</div>
</div>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="HarvestMultiplier">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Harvest multiplier</span>
<input id="AdminMassPlantHarvestMultiplier" type="number" min="0" max="9999.9999" step="0.0001" value="1.5">
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="ShopPlant">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Shop plant</span>
<select id="AdminMassPlantShopPlant">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</label>
</div>
<div class="AdminMassField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="BaseCost">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Base seed cost</span>
<input id="AdminMassPlantBaseCost" type="number" min="0" step="1" value="0">
</label>
</div>
<div class="AdminMassField AdminEditorWideField">
<label class="AdminEditorCheckField AdminMassApplyField">
<input type="checkbox" data-admin-mass-apply="Effects">
<span>Apply</span>
</label>
<label class="AdminEditorField">
<span>Effects JSON</span>
<textarea id="AdminMassPlantEffects" rows="8" spellcheck="false">{}</textarea>
</label>
</div>
</div>
<div class="AdminEditorActions">
<button
id="AdminMassPlantSaveButton"
class="ActionButton AdminSaveButton"
type="submit"
>
Save marked plants
</button>
<a
class="ActionButton AdminInlineButton AdminEditorBackLink"
href="/Pages/Admin.html"
>
Back to Admin
</a>
</div>
</form>
<p
id="AdminMassPlantMessage"
class="PageMessage"
aria-live="polite"
></p>
</div>
</main>
<script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/AdminValidation.js"></script>
<script src="/Scripts/Garden/AdminMassPlantEditor.js"></script>
</body>
</html>
+26
View File
@@ -64,6 +64,31 @@ $PageSection = "Admin";
</button>
</div>
<div class="AdminEditorActions AdminEditorTransferActions">
<button
id="AdminMutationImportButton"
class="ActionButton AdminInlineButton"
type="button"
>
Import JSON
</button>
<input
id="AdminMutationImportFile"
type="file"
accept="application/json,.json"
hidden
>
<button
id="AdminMutationExportButton"
class="ActionButton AdminInlineButton"
type="button"
>
Export JSON
</button>
</div>
<form
id="AdminMutationForm"
class="Panel AdminEditorForm"
@@ -489,6 +514,7 @@ $PageSection = "Admin";
<script src="/Scripts/Garden/Plants.js"></script>
<script src="/Scripts/Garden/Mutations.js"></script>
<script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/AdminValidation.js"></script>
<script src="/Scripts/Garden/AdminMutationEditor.js"></script>
</body>
</html>
+26
View File
@@ -64,6 +64,31 @@ $PageSection = "Admin";
</button>
</div>
<div class="AdminEditorActions AdminEditorTransferActions">
<button
id="AdminPlantImportButton"
class="ActionButton AdminInlineButton"
type="button"
>
Import JSON
</button>
<input
id="AdminPlantImportFile"
type="file"
accept="application/json,.json"
hidden
>
<button
id="AdminPlantExportButton"
class="ActionButton AdminInlineButton"
type="button"
>
Export JSON
</button>
</div>
<form
id="AdminPlantForm"
class="Panel AdminEditorForm"
@@ -360,6 +385,7 @@ $PageSection = "Admin";
<script src="/Scripts/Garden/Mutations.js"></script>
<script src="/Scripts/Garden/Save.js"></script>
<script src="/Scripts/Garden/Economy.js"></script>
<script src="/Scripts/Garden/AdminValidation.js"></script>
<script src="/Scripts/Garden/AdminPlantEditor.js"></script>
</body>
</html>
+787
View File
@@ -0,0 +1,787 @@
let AdminMassMutationCatalogue = {};
let AdminMassMutationPlantCatalogue = {};
let AdminMassMutationSavePending = false;
async function AdminMassMutationRequest(
Endpoint,
Action,
Data = {}
) {
const SaveKey =
localStorage.getItem(
SaveKeyName
);
if (
typeof SaveKey !== "string" ||
!/^[0-9A-Fa-f]{64}$/.test(
SaveKey
)
) {
throw new Error(
"A valid Account Key is required."
);
}
const Response = await fetch(
ApiUrl + "/" + Endpoint,
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
SaveKey: SaveKey,
Action: Action,
...Data
})
}
);
const Result =
await Response.json();
if (
!Response.ok ||
Result.Success !== true
) {
throw new Error(
Result.Error ??
"Mutation admin request failed."
);
}
return Result;
}
function GetSortedAdminMassMutationKeys() {
return Object.keys(
AdminMassMutationCatalogue
).sort(
(A, B) =>
Number(
AdminMassMutationCatalogue[A]
?.Id ?? 0
) -
Number(
AdminMassMutationCatalogue[B]
?.Id ?? 0
) ||
A.localeCompare(B)
);
}
function RenderAdminMassMutationSelection() {
const Container =
document.getElementById(
"AdminMassMutationSelection"
);
Container.replaceChildren();
for (
const MutationKey
of GetSortedAdminMassMutationKeys()
) {
const Mutation =
AdminMassMutationCatalogue[
MutationKey
];
const Label =
document.createElement("label");
Label.className =
"AdminPlantChecklistItem";
const Input =
document.createElement("input");
Input.type = "checkbox";
Input.value = MutationKey;
Input.dataset.adminMassTarget =
"mutation";
const Text =
document.createElement("span");
Text.textContent =
String(Mutation.Id).padStart(
3,
"0"
) +
" — " +
(Mutation.Name ?? MutationKey) +
(
Mutation.Archived === true
? " (archived)"
: ""
);
Label.append(Input, Text);
Container.appendChild(Label);
}
}
function SetAdminMassMutationSelection(
Checked
) {
for (
const Input
of document.querySelectorAll(
'[data-admin-mass-target="mutation"]'
)
) {
Input.checked = Checked;
}
}
function UpdateAdminMassMutationFieldStates() {
const FieldMap = {
Name: ["AdminMassMutationName"],
Description: [
"AdminMassMutationDescription"
],
Hint: ["AdminMassMutationHint"],
Priority: [
"AdminMassMutationPriority"
],
Chance: ["AdminMassMutationChance"],
Cooldown: [
"AdminMassMutationCooldownHours",
"AdminMassMutationCooldownMinutes",
"AdminMassMutationCooldownSeconds"
],
Rotation: [
"AdminMassMutationRotation"
],
AllowImmature: [
"AdminMassMutationAllowImmature"
],
Pattern: [
"AdminMassMutationPattern"
],
Success: [
"AdminMassMutationSuccess"
],
Failure: [
"AdminMassMutationFailure"
],
PlantsUsed: [
"AdminMassMutationPlantsUsed"
],
PlantsCreated: [
"AdminMassMutationPlantsCreated"
]
};
for (
const Apply
of document.querySelectorAll(
"[data-admin-mass-apply]"
)
) {
const FieldName =
Apply.dataset.adminMassApply;
for (
const ElementId
of FieldMap[FieldName] ?? []
) {
document.getElementById(
ElementId
).disabled = !Apply.checked;
}
}
}
function AdminMassMutationFieldEnabled(
FieldName
) {
return document.querySelector(
'[data-admin-mass-apply="' +
FieldName +
'"]'
)?.checked === true;
}
function GetAdminMassMutationSelectedKeys() {
return [
...document.querySelectorAll(
'[data-admin-mass-target="mutation"]:checked'
)
].map(Input => Input.value);
}
function GetAdminMassMutationCooldown() {
const Hours = Number(
document.getElementById(
"AdminMassMutationCooldownHours"
).value
);
const Minutes = Number(
document.getElementById(
"AdminMassMutationCooldownMinutes"
).value
);
const Seconds = Number(
document.getElementById(
"AdminMassMutationCooldownSeconds"
).value
);
if (
!Number.isInteger(Hours) ||
!Number.isInteger(Minutes) ||
!Number.isInteger(Seconds) ||
Hours < 0 ||
Minutes < 0 ||
Minutes > 59 ||
Seconds < 0 ||
Seconds > 59
) {
throw new Error(
"Cooldown is invalid."
);
}
return (
Hours * 3600000 +
Minutes * 60000 +
Seconds * 1000
);
}
function ParseAdminMassMutationJson(
ElementId,
Name
) {
const Raw =
document.getElementById(
ElementId
).value.trim();
try {
return JSON.parse(Raw);
} catch (Error) {
throw new Error(
Name + " is not valid JSON."
);
}
}
function ParseAdminMassMutationPlantKeys(
ElementId,
Name
) {
const Keys = [
...new Set(
document.getElementById(
ElementId
).value
.split(",")
.map(Value => Value.trim())
.filter(Value => Value !== "")
)
];
return Keys.map(
PlantKey => {
const Plant =
AdminMassMutationPlantCatalogue[
PlantKey
];
if (Plant === undefined) {
throw new Error(
Name +
" references unknown Plant Key: " +
PlantKey
);
}
return Number(Plant.Id);
}
).sort((A, B) => A - B);
}
function GetAdminMassMutationPatch() {
const Patch = {};
if (AdminMassMutationFieldEnabled("Name")) {
Patch.Name =
document.getElementById(
"AdminMassMutationName"
).value.trim();
}
if (
AdminMassMutationFieldEnabled(
"Description"
)
) {
Patch.Description =
document.getElementById(
"AdminMassMutationDescription"
).value.trim();
}
if (AdminMassMutationFieldEnabled("Hint")) {
Patch.Hint =
document.getElementById(
"AdminMassMutationHint"
).value.trim();
}
if (
AdminMassMutationFieldEnabled(
"Priority"
)
) {
Patch.Priority =
Number(
document.getElementById(
"AdminMassMutationPriority"
).value
);
}
if (
AdminMassMutationFieldEnabled(
"Chance"
)
) {
Patch.Chance =
Number(
document.getElementById(
"AdminMassMutationChance"
).value
) / 100;
}
if (
AdminMassMutationFieldEnabled(
"Cooldown"
)
) {
Patch.Cooldown =
GetAdminMassMutationCooldown();
}
if (
AdminMassMutationFieldEnabled(
"Rotation"
)
) {
Patch.Rotation =
document.getElementById(
"AdminMassMutationRotation"
).value;
}
if (
AdminMassMutationFieldEnabled(
"AllowImmature"
)
) {
Patch.AllowImmature =
document.getElementById(
"AdminMassMutationAllowImmature"
).value === "true";
}
if (
AdminMassMutationFieldEnabled(
"Pattern"
)
) {
Patch.Pattern =
ParseAdminMassMutationJson(
"AdminMassMutationPattern",
"Pattern JSON"
);
}
if (
AdminMassMutationFieldEnabled(
"Success"
)
) {
Patch.Success =
ParseAdminMassMutationJson(
"AdminMassMutationSuccess",
"Success result JSON"
);
}
if (
AdminMassMutationFieldEnabled(
"Failure"
)
) {
Patch.Failure =
ParseAdminMassMutationJson(
"AdminMassMutationFailure",
"Failure result JSON"
);
}
if (
AdminMassMutationFieldEnabled(
"PlantsUsed"
)
) {
Patch.PlantsUsed =
ParseAdminMassMutationPlantKeys(
"AdminMassMutationPlantsUsed",
"Plants used"
);
}
if (
AdminMassMutationFieldEnabled(
"PlantsCreated"
)
) {
Patch.PlantsCreated =
ParseAdminMassMutationPlantKeys(
"AdminMassMutationPlantsCreated",
"Plants created"
);
}
return Patch;
}
function BuildAdminMassMutationPayload(
MutationKey,
Patch
) {
const Source =
AdminMassMutationCatalogue[
MutationKey
];
const Mutation = {
Id: Number(Source.Id),
MutationKey: MutationKey,
Name: Source.Name ?? "",
Description:
Source.Description ?? "",
Hint: Source.Hint ?? "",
Priority:
Number(Source.Priority ?? 0),
Chance:
Number(Source.Chance ?? 0),
Cooldown:
Number(Source.Cooldown ?? 0),
Rotation:
Source.Rotation ?? "None",
AllowImmature:
Source.AllowImmature === true,
Pattern:
JSON.parse(
JSON.stringify(
Source.Pattern ?? []
)
),
Success:
JSON.parse(
JSON.stringify(
Source.Success ?? []
)
),
Failure:
JSON.parse(
JSON.stringify(
Source.Failure ?? "Keep"
)
),
Relations: {
PlantsUsed: [
...(Source.Relations
?.PlantsUsed ?? [])
],
PlantsCreated: [
...(Source.Relations
?.PlantsCreated ?? [])
]
},
IsNew: false
};
for (
const [Key, Value]
of Object.entries(Patch)
) {
if (
Key === "PlantsUsed" ||
Key === "PlantsCreated"
) {
Mutation.Relations[Key] = [
...Value
];
continue;
}
Mutation[Key] =
Value !== null &&
typeof Value === "object"
? JSON.parse(
JSON.stringify(Value)
)
: Value;
}
return Mutation;
}
async function SaveAdminMassMutations(
Event
) {
Event.preventDefault();
if (AdminMassMutationSavePending) {
return;
}
const SelectedKeys =
GetAdminMassMutationSelectedKeys();
if (SelectedKeys.length === 0) {
SetAdminMassMutationMessage(
"Mark at least one mutation first."
);
return;
}
let Patch;
try {
Patch =
GetAdminMassMutationPatch();
} catch (Error) {
SetAdminMassMutationMessage(
Error.message
);
return;
}
if (Object.keys(Patch).length === 0) {
SetAdminMassMutationMessage(
"Enable at least one field to update."
);
return;
}
const Payloads = [];
try {
for (
const MutationKey
of SelectedKeys
) {
const Mutation =
BuildAdminMassMutationPayload(
MutationKey,
Patch
);
ValidateAdminMutation(
Mutation,
AdminMassMutationCatalogue,
AdminMassMutationPlantCatalogue,
MutationKey
);
Payloads.push(Mutation);
}
} catch (Error) {
SetAdminMassMutationMessage(
Error.message
);
return;
}
AdminMassMutationSavePending = true;
const SaveButton =
document.getElementById(
"AdminMassMutationSaveButton"
);
SaveButton.disabled = true;
let SavedCount = 0;
try {
for (const Mutation of Payloads) {
SetAdminMassMutationMessage(
"Saving " +
(SavedCount + 1) +
" of " +
Payloads.length +
"..."
);
await AdminMassMutationRequest(
"AdminMutations.php",
"Save",
{Mutation: Mutation}
);
SavedCount++;
}
localStorage.removeItem(
"SarahtoninGardenContent"
);
await LoadAdminMassMutationData();
SetAdminMassMutationMessage(
SavedCount === 1
? "1 mutation updated."
: SavedCount +
" mutations updated."
);
} catch (Error) {
SetAdminMassMutationMessage(
"Saved " +
SavedCount +
" before stopping: " +
(Error.message ??
"Couldn't save mutations.")
);
} finally {
AdminMassMutationSavePending = false;
SaveButton.disabled = false;
}
}
function BindAdminMassMutationEditor() {
document.getElementById(
"AdminMassMutationSelectAll"
).addEventListener(
"click",
() =>
SetAdminMassMutationSelection(
true
)
);
document.getElementById(
"AdminMassMutationClear"
).addEventListener(
"click",
() =>
SetAdminMassMutationSelection(
false
)
);
document.getElementById(
"AdminMassMutationForm"
).addEventListener(
"submit",
SaveAdminMassMutations
);
for (
const Apply
of document.querySelectorAll(
"[data-admin-mass-apply]"
)
) {
Apply.addEventListener(
"change",
UpdateAdminMassMutationFieldStates
);
}
UpdateAdminMassMutationFieldStates();
}
async function LoadAdminMassMutationData() {
const [MutationResult, PlantResult] =
await Promise.all([
AdminMassMutationRequest(
"AdminMutations.php",
"List"
),
AdminMassMutationRequest(
"AdminPlants.php",
"List"
)
]);
AdminMassMutationCatalogue =
MutationResult.Mutations ?? {};
AdminMassMutationPlantCatalogue =
PlantResult.Plants ?? {};
RenderAdminMassMutationSelection();
}
function SetAdminMassMutationMessage(
Message
) {
document.getElementById(
"AdminMassMutationMessage"
).textContent = Message;
}
async function StartAdminMassMutationEditor() {
try {
const Status =
await GetAdminStatus();
if (
Status.Success !== true ||
Status.IsAdmin !== true
) {
SetAdminMassMutationMessage(
"Admin access required."
);
return;
}
BindAdminMassMutationEditor();
await LoadAdminMassMutationData();
SetAdminMassMutationMessage(
"Mark mutations and enable the fields you want to change."
);
} catch (Error) {
console.error(
"Couldn't start Mass mutation editor:",
Error
);
SetAdminMassMutationMessage(
Error.message ??
"Couldn't load the Mass mutation editor."
);
}
}
document.addEventListener(
"DOMContentLoaded",
StartAdminMassMutationEditor
);
+645
View File
@@ -0,0 +1,645 @@
let AdminMassPlantCatalogue = {};
let AdminMassPlantSavePending = false;
async function AdminMassPlantRequest(
Action,
Data = {}
) {
const SaveKey =
localStorage.getItem(
SaveKeyName
);
if (
typeof SaveKey !== "string" ||
!/^[0-9A-Fa-f]{64}$/.test(
SaveKey
)
) {
throw new Error(
"A valid Account Key is required."
);
}
const Response = await fetch(
ApiUrl + "/AdminPlants.php",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
SaveKey: SaveKey,
Action: Action,
...Data
})
}
);
const Result =
await Response.json();
if (
!Response.ok ||
Result.Success !== true
) {
throw new Error(
Result.Error ??
"Plant admin request failed."
);
}
return Result;
}
function GetSortedAdminMassPlantKeys() {
return Object.keys(
AdminMassPlantCatalogue
).sort(
(A, B) =>
Number(
AdminMassPlantCatalogue[A]
?.Id ?? 0
) -
Number(
AdminMassPlantCatalogue[B]
?.Id ?? 0
) ||
A.localeCompare(B)
);
}
function RenderAdminMassPlantSelection() {
const Container =
document.getElementById(
"AdminMassPlantSelection"
);
Container.replaceChildren();
for (
const PlantKey
of GetSortedAdminMassPlantKeys()
) {
const Plant =
AdminMassPlantCatalogue[
PlantKey
];
const Label =
document.createElement("label");
Label.className =
"AdminPlantChecklistItem";
const Input =
document.createElement("input");
Input.type = "checkbox";
Input.value = PlantKey;
Input.dataset.adminMassTarget =
"plant";
const Text =
document.createElement("span");
Text.textContent =
String(Plant.Id).padStart(
3,
"0"
) +
" — " +
(Plant.Name ?? PlantKey) +
(
Plant.Archived === true
? " (archived)"
: ""
);
Label.append(
Input,
Text
);
Container.appendChild(Label);
}
}
function SetAdminMassPlantSelection(
Checked
) {
for (
const Input
of document.querySelectorAll(
'[data-admin-mass-target="plant"]'
)
) {
Input.checked = Checked;
}
}
function UpdateAdminMassPlantFieldStates() {
const FieldMap = {
Name: [
"AdminMassPlantName"
],
Description: [
"AdminMassPlantDescription"
],
Tags: [
"AdminMassPlantTags"
],
GrowthTime: [
"AdminMassPlantGrowthHours",
"AdminMassPlantGrowthMinutes",
"AdminMassPlantGrowthSeconds"
],
HarvestMultiplier: [
"AdminMassPlantHarvestMultiplier"
],
ShopPlant: [
"AdminMassPlantShopPlant"
],
BaseCost: [
"AdminMassPlantBaseCost"
],
Effects: [
"AdminMassPlantEffects"
]
};
for (
const Apply
of document.querySelectorAll(
"[data-admin-mass-apply]"
)
) {
const FieldName =
Apply.dataset.adminMassApply;
for (
const ElementId
of FieldMap[FieldName] ?? []
) {
document.getElementById(
ElementId
).disabled = !Apply.checked;
}
}
}
function GetAdminMassPlantSelectedKeys() {
return [
...document.querySelectorAll(
'[data-admin-mass-target="plant"]:checked'
)
].map(Input => Input.value);
}
function AdminMassPlantFieldEnabled(
FieldName
) {
return document.querySelector(
'[data-admin-mass-apply="' +
FieldName +
'"]'
)?.checked === true;
}
function ParseAdminMassPlantTags() {
return [
...new Set(
document.getElementById(
"AdminMassPlantTags"
).value
.split(",")
.map(Tag => Tag.trim())
.filter(Tag => Tag !== "")
)
];
}
function GetAdminMassPlantGrowthTime() {
const Hours = Number(
document.getElementById(
"AdminMassPlantGrowthHours"
).value
);
const Minutes = Number(
document.getElementById(
"AdminMassPlantGrowthMinutes"
).value
);
const Seconds = Number(
document.getElementById(
"AdminMassPlantGrowthSeconds"
).value
);
if (
!Number.isInteger(Hours) ||
!Number.isInteger(Minutes) ||
!Number.isInteger(Seconds) ||
Hours < 0 ||
Minutes < 0 ||
Minutes > 59 ||
Seconds < 0 ||
Seconds > 59
) {
throw new Error(
"Growth time is invalid."
);
}
return (
Hours * 3600000 +
Minutes * 60000 +
Seconds * 1000
);
}
function GetAdminMassPlantEffects() {
const Raw =
document.getElementById(
"AdminMassPlantEffects"
).value.trim();
const Effects = JSON.parse(
Raw === "" ? "{}" : Raw
);
if (!IsAdminPlainObject(Effects)) {
throw new Error(
"Effects JSON must be an object."
);
}
return Effects;
}
function GetAdminMassPlantPatch() {
const Patch = {};
if (AdminMassPlantFieldEnabled("Name")) {
Patch.Name =
document.getElementById(
"AdminMassPlantName"
).value.trim();
}
if (
AdminMassPlantFieldEnabled(
"Description"
)
) {
Patch.Description =
document.getElementById(
"AdminMassPlantDescription"
).value.trim();
}
if (AdminMassPlantFieldEnabled("Tags")) {
Patch.Tags =
ParseAdminMassPlantTags();
}
if (
AdminMassPlantFieldEnabled(
"GrowthTime"
)
) {
Patch.GrowthTime =
GetAdminMassPlantGrowthTime();
}
if (
AdminMassPlantFieldEnabled(
"HarvestMultiplier"
)
) {
Patch.HarvestMultiplier =
Number(
document.getElementById(
"AdminMassPlantHarvestMultiplier"
).value
);
}
if (
AdminMassPlantFieldEnabled(
"ShopPlant"
)
) {
Patch.ShopPlant =
document.getElementById(
"AdminMassPlantShopPlant"
).value === "true";
}
if (
AdminMassPlantFieldEnabled(
"BaseCost"
)
) {
Patch.BaseCost =
Number(
document.getElementById(
"AdminMassPlantBaseCost"
).value
);
}
if (
AdminMassPlantFieldEnabled(
"Effects"
)
) {
Patch.Effects =
GetAdminMassPlantEffects();
}
return Patch;
}
function BuildAdminMassPlantPayload(
PlantKey,
Patch
) {
const Source =
AdminMassPlantCatalogue[
PlantKey
];
const Plant = {
Id: Number(Source.Id),
PlantKey: PlantKey,
Name: Source.Name ?? "",
Description:
Source.Description ?? "",
Tags:
Array.isArray(Source.Tags)
? [...Source.Tags]
: [],
GrowthTime:
Number(Source.GrowthTime ?? 0),
HarvestMultiplier:
Number(
Source.HarvestMultiplier ??
1.5
),
Effects:
JSON.parse(
JSON.stringify(
Source.Effects ?? {}
)
),
ShopPlant:
Source.Shop?.ShopPlant === true,
BaseCost:
Source.Shop?.BaseCost ?? null,
IsNew: false
};
Object.assign(
Plant,
JSON.parse(
JSON.stringify(Patch)
)
);
return Plant;
}
async function SaveAdminMassPlants(
Event
) {
Event.preventDefault();
if (AdminMassPlantSavePending) {
return;
}
const SelectedKeys =
GetAdminMassPlantSelectedKeys();
if (SelectedKeys.length === 0) {
SetAdminMassPlantMessage(
"Mark at least one plant first."
);
return;
}
let Patch;
try {
Patch = GetAdminMassPlantPatch();
} catch (Error) {
SetAdminMassPlantMessage(
Error.message
);
return;
}
if (Object.keys(Patch).length === 0) {
SetAdminMassPlantMessage(
"Enable at least one field to update."
);
return;
}
const Payloads = [];
try {
for (const PlantKey of SelectedKeys) {
const Plant =
BuildAdminMassPlantPayload(
PlantKey,
Patch
);
ValidateAdminPlant(
Plant,
AdminMassPlantCatalogue,
PlantKey
);
Payloads.push(Plant);
}
} catch (Error) {
SetAdminMassPlantMessage(
Error.message
);
return;
}
AdminMassPlantSavePending = true;
const SaveButton =
document.getElementById(
"AdminMassPlantSaveButton"
);
SaveButton.disabled = true;
let SavedCount = 0;
try {
for (const Plant of Payloads) {
SetAdminMassPlantMessage(
"Saving " +
(SavedCount + 1) +
" of " +
Payloads.length +
"..."
);
await AdminMassPlantRequest(
"Save",
{Plant: Plant}
);
SavedCount++;
}
localStorage.removeItem(
"SarahtoninGardenContent"
);
await LoadAdminMassPlants();
SetAdminMassPlantMessage(
SavedCount === 1
? "1 plant updated."
: SavedCount +
" plants updated."
);
} catch (Error) {
SetAdminMassPlantMessage(
"Saved " +
SavedCount +
" before stopping: " +
(Error.message ??
"Couldn't save plants.")
);
} finally {
AdminMassPlantSavePending = false;
SaveButton.disabled = false;
}
}
function BindAdminMassPlantEditor() {
document.getElementById(
"AdminMassPlantSelectAll"
).addEventListener(
"click",
() =>
SetAdminMassPlantSelection(
true
)
);
document.getElementById(
"AdminMassPlantClear"
).addEventListener(
"click",
() =>
SetAdminMassPlantSelection(
false
)
);
document.getElementById(
"AdminMassPlantForm"
).addEventListener(
"submit",
SaveAdminMassPlants
);
for (
const Apply
of document.querySelectorAll(
"[data-admin-mass-apply]"
)
) {
Apply.addEventListener(
"change",
UpdateAdminMassPlantFieldStates
);
}
UpdateAdminMassPlantFieldStates();
}
async function LoadAdminMassPlants() {
const Result =
await AdminMassPlantRequest(
"List"
);
AdminMassPlantCatalogue =
Result.Plants ?? {};
RenderAdminMassPlantSelection();
}
function SetAdminMassPlantMessage(
Message
) {
document.getElementById(
"AdminMassPlantMessage"
).textContent = Message;
}
async function StartAdminMassPlantEditor() {
try {
const Status =
await GetAdminStatus();
if (
Status.Success !== true ||
Status.IsAdmin !== true
) {
SetAdminMassPlantMessage(
"Admin access required."
);
return;
}
BindAdminMassPlantEditor();
await LoadAdminMassPlants();
SetAdminMassPlantMessage(
"Mark plants and enable the fields you want to change."
);
} catch (Error) {
console.error(
"Couldn't start Mass plant editor:",
Error
);
SetAdminMassPlantMessage(
Error.message ??
"Couldn't load the Mass plant editor."
);
}
}
document.addEventListener(
"DOMContentLoaded",
StartAdminMassPlantEditor
);
+492
View File
@@ -80,6 +80,31 @@ function BindAdminMutationEditor() {
DuplicateAdminMutation
);
document.getElementById(
"AdminMutationImportButton"
).addEventListener(
"click",
() => {
document.getElementById(
"AdminMutationImportFile"
).click();
}
);
document.getElementById(
"AdminMutationImportFile"
).addEventListener(
"change",
ImportAdminMutationJson
);
document.getElementById(
"AdminMutationExportButton"
).addEventListener(
"click",
ExportAdminMutationJson
);
document.getElementById(
"AdminMutationResizeButton"
).addEventListener(
@@ -2339,6 +2364,21 @@ async function SaveAdminMutation(
return;
}
try {
ValidateAdminMutation(
Mutation,
AdminMutationCatalogue,
AdminMutationPlantCatalogue,
AdminMutationEditingKey
);
} catch (Error) {
SetAdminMutationMessage(
Error.message
);
return;
}
AdminMutationSavePending = true;
const SaveButton =
@@ -2419,6 +2459,458 @@ function RenderAdminMutationJsonPreview() {
}
function GetAdminMutationPortableRelations(
Relations
) {
const Result = {
PlantsUsed: [],
PlantsCreated: []
};
for (
const RelationName
of [
"PlantsUsed",
"PlantsCreated"
]
) {
for (
const PlantId
of Relations?.[
RelationName
] ?? []
) {
const PlantKey =
GetAdminPlantKeyById(
PlantId,
AdminMutationPlantCatalogue
);
if (PlantKey !== null) {
Result[RelationName].push(
PlantKey
);
}
}
}
return Result;
}
function GetAdminMutationPortableData(
Mutation
) {
return {
MutationKey:
Mutation.MutationKey,
Name: Mutation.Name,
Description:
Mutation.Description,
Hint:
Mutation.Hint === ""
? null
: Mutation.Hint,
Priority: Mutation.Priority,
Chance: Mutation.Chance,
Cooldown: Mutation.Cooldown,
Rotation: Mutation.Rotation,
AllowImmature:
Mutation.AllowImmature === true,
Pattern:
CloneAdminMutationValue(
Mutation.Pattern
),
Success:
CloneAdminMutationValue(
Mutation.Success
),
Failure:
CloneAdminMutationValue(
Mutation.Failure
),
Relations:
GetAdminMutationPortableRelations(
Mutation.Relations
)
};
}
function ExportAdminMutationJson() {
const Form =
document.getElementById(
"AdminMutationForm"
);
if (!Form.reportValidity()) {
return;
}
try {
const Mutation =
GetAdminMutationFormData();
ValidateAdminMutation(
Mutation,
AdminMutationCatalogue,
AdminMutationPlantCatalogue,
AdminMutationEditingKey
);
DownloadAdminJson(
"Mutation-" +
Mutation.MutationKey +
".json",
{
Type: "Mutation",
Version: 1,
Mutation:
GetAdminMutationPortableData(
Mutation
)
}
);
SetAdminMutationMessage(
"Mutation JSON exported. Numeric IDs are not included."
);
} catch (Error) {
SetAdminMutationMessage(
Error.message ??
"Couldn't export mutation JSON."
);
}
}
function ConvertAdminMutationPortableRelations(
Relations
) {
if (
Relations === undefined ||
Relations === null
) {
return {
PlantsUsed: [],
PlantsCreated: []
};
}
if (!IsAdminPlainObject(Relations)) {
throw new Error(
"Relations must be an object."
);
}
const Result = {
PlantsUsed: [],
PlantsCreated: []
};
for (
const RelationName
of [
"PlantsUsed",
"PlantsCreated"
]
) {
const Values =
Relations[RelationName] ?? [];
if (!Array.isArray(Values)) {
throw new Error(
RelationName +
" must be an array of Plant Keys."
);
}
for (const Value of Values) {
if (typeof Value !== "string") {
throw new Error(
RelationName +
" must use Plant Keys, not numeric IDs."
);
}
const Plant =
AdminMutationPlantCatalogue[
Value
];
if (Plant === undefined) {
throw new Error(
RelationName +
" references unknown Plant Key: " +
Value
);
}
Result[RelationName].push(
Number(Plant.Id)
);
}
Result[RelationName] = [
...new Set(
Result[RelationName]
)
].sort(
(A, B) => A - B
);
}
return Result;
}
async function ImportAdminMutationJson(
Event
) {
const Input = Event.target;
const File = Input.files?.[0];
Input.value = "";
if (File === undefined) {
return;
}
try {
const DocumentData =
await ReadAdminJsonFile(
File
);
if (
IsAdminPlainObject(
DocumentData
) &&
DocumentData.Type !==
undefined &&
DocumentData.Type !== "Mutation"
) {
throw new Error(
"That JSON file is not a mutation export."
);
}
if (
IsAdminPlainObject(
DocumentData
) &&
DocumentData.Version !==
undefined &&
Number(DocumentData.Version) !== 1
) {
throw new Error(
"Unsupported mutation JSON version."
);
}
const Imported =
IsAdminPlainObject(
DocumentData?.Mutation
)
? DocumentData.Mutation
: DocumentData;
if (!IsAdminPlainObject(Imported)) {
throw new Error(
"Mutation JSON must contain a mutation object."
);
}
const MutationKey =
String(
Imported.MutationKey ?? ""
).trim();
const Existing =
AdminMutationCatalogue[
MutationKey
];
const Mutation = {
Id:
Existing?.Id ??
GetNextAdminMutationId(),
MutationKey: MutationKey,
Name:
String(
Imported.Name ?? ""
).trim(),
Description:
typeof Imported.Description ===
"string"
? Imported.Description.trim()
: "",
Hint:
typeof Imported.Hint ===
"string"
? Imported.Hint.trim()
: "",
Priority:
Number(
Imported.Priority ?? 0
),
Chance:
Number(
Imported.Chance
),
Cooldown:
Number(
Imported.Cooldown
),
Rotation:
Imported.Rotation ??
"None",
AllowImmature:
Imported.AllowImmature ===
true,
Pattern:
CloneAdminMutationValue(
Imported.Pattern
),
Success:
CloneAdminMutationValue(
Imported.Success
),
Failure:
CloneAdminMutationValue(
Imported.Failure ??
"Keep"
),
Relations:
ConvertAdminMutationPortableRelations(
Imported.Relations
),
IsNew:
Existing === undefined
};
ValidateAdminMutation(
Mutation,
AdminMutationCatalogue,
AdminMutationPlantCatalogue,
Existing === undefined
? null
: MutationKey
);
if (Existing === undefined) {
StartNewAdminMutation();
AdminMutationEditingKey = null;
} else {
LoadAdminMutationIntoForm(
MutationKey
);
}
SetAdminMutationField(
"AdminMutationId",
Mutation.Id
);
SetAdminMutationField(
"AdminMutationKey",
Mutation.MutationKey
);
SetAdminMutationField(
"AdminMutationName",
Mutation.Name
);
SetAdminMutationField(
"AdminMutationDescription",
Mutation.Description
);
SetAdminMutationField(
"AdminMutationHint",
Mutation.Hint
);
SetAdminMutationField(
"AdminMutationPriority",
Mutation.Priority
);
SetAdminMutationField(
"AdminMutationChance",
Mutation.Chance * 100
);
SetAdminMutationDurationFields(
Mutation.Cooldown
);
SetAdminMutationField(
"AdminMutationRotation",
Mutation.Rotation
);
document.getElementById(
"AdminMutationAllowImmature"
).checked =
Mutation.AllowImmature;
AdminMutationPattern =
CloneAdminMutationMatrix(
Mutation.Pattern,
"Any"
);
AdminMutationResult =
NormalizeAdminMutationMatrix(
Mutation.Success,
AdminMutationPattern[0]
.length,
AdminMutationPattern.length,
"Keep"
);
SetAdminMutationField(
"AdminMutationWidth",
AdminMutationPattern[0]
.length
);
SetAdminMutationField(
"AdminMutationHeight",
AdminMutationPattern.length
);
LoadAdminMutationFailure(
Mutation.Failure
);
SetAdminMutationRelations(
Mutation.Relations
);
document.getElementById(
"AdminMutationId"
).readOnly =
Existing !== undefined;
document.getElementById(
"AdminMutationKey"
).readOnly =
Existing !== undefined;
AdminMutationSelectedCell = null;
HideAdminMutationCellEditor();
RenderAdminMutationGrids();
UpdateAdminMutationFailureVisibility();
UpdateAdminMutationCooldownHint();
RenderAdminMutationJsonPreview();
SetAdminMutationMessage(
Existing === undefined
? "Mutation JSON imported as a new mutation. Review it, then save when ready."
: "Mutation JSON imported over " +
MutationKey +
". Review it, then save when ready."
);
} catch (Error) {
SetAdminMutationMessage(
Error.message ??
"Couldn't import mutation JSON."
);
}
}
function SetAdminMutationDurationFields(
Milliseconds
) {
+485
View File
@@ -0,0 +1,485 @@
let AdminOverviewPlants = {};
let AdminOverviewMutations = {};
async function AdminOverviewRequest(
Endpoint,
Action = "List"
) {
const SaveKey =
GetStoredAdminSaveKey();
if (SaveKey === null) {
throw new Error(
"A valid Account Key is required."
);
}
const Response = await fetch(
AdminApiUrl + "/" + Endpoint,
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
SaveKey: SaveKey,
Action: Action
})
}
);
const Result =
await Response.json();
if (
!Response.ok ||
Result.Success !== true
) {
throw new Error(
Result.Error ??
"Admin overview request failed."
);
}
return Result;
}
function GetAdminOverviewPlantKeyById(
PlantId
) {
return Object.entries(
AdminOverviewPlants
).find(
([, Plant]) =>
Number(Plant.Id) ===
Number(PlantId)
)?.[0] ?? null;
}
function GetAdminOverviewMissingSprites(
PlantKey,
Plant
) {
const StaticImages =
Array.isArray(
PlantImages?.[PlantKey]
)
? PlantImages[PlantKey]
: [];
const ApiStages =
new Set(
(
Array.isArray(
Plant.ImageStages
)
? Plant.ImageStages
: []
)
.map(
Stage =>
Number(
Stage?.Stage ??
Stage
)
)
.filter(
Stage =>
Number.isInteger(Stage) &&
Stage > 0
)
);
const HighestApiStage =
Math.max(
0,
...ApiStages
);
const HighestStage =
Math.max(
3,
StaticImages.length,
HighestApiStage
);
const Missing = [];
for (
let Stage = 1;
Stage <= HighestStage;
Stage++
) {
if (
ApiStages.has(Stage) ||
typeof StaticImages[
Stage - 1
] === "string"
) {
continue;
}
Missing.push(
Stage + ".png"
);
}
return Missing;
}
function CollectAdminOverviewCreatedPlants() {
const Created = new Set();
function AddResultValue(Value) {
if (typeof Value === "string") {
if (
Value !== "Keep" &&
Value !== "Empty" &&
!Value.startsWith("$") &&
AdminOverviewPlants[
Value
] !== undefined
) {
Created.add(Value);
}
return;
}
if (
Value !== null &&
typeof Value === "object" &&
!Array.isArray(Value) &&
typeof Value.Plant === "string"
) {
AddResultValue(
Value.Plant
);
}
}
for (
const Mutation
of Object.values(
AdminOverviewMutations
)
) {
if (Mutation.Archived === true) {
continue;
}
for (
const PlantId
of Mutation.Relations
?.PlantsCreated ?? []
) {
const PlantKey =
GetAdminOverviewPlantKeyById(
PlantId
);
if (PlantKey !== null) {
Created.add(PlantKey);
}
}
for (
const Row
of Mutation.Success ?? []
) {
if (!Array.isArray(Row)) {
continue;
}
for (const Cell of Row) {
AddResultValue(Cell);
}
}
}
return Created;
}
function CreateAdminOverviewIssueList(
Items,
NestedValues = null
) {
const List =
document.createElement("ul");
List.className =
"AdminOverviewList";
for (const Item of Items) {
const ListItem =
document.createElement("li");
const Label =
document.createElement("span");
Label.textContent = Item.Label;
ListItem.appendChild(Label);
const Nested =
NestedValues?.(Item) ?? [];
if (Nested.length > 0) {
const NestedList =
document.createElement("ul");
for (const Value of Nested) {
const NestedItem =
document.createElement("li");
NestedItem.textContent =
Value;
NestedList.appendChild(
NestedItem
);
}
ListItem.appendChild(
NestedList
);
}
List.appendChild(ListItem);
}
return List;
}
function RenderAdminOverviewSection(
ContainerId,
Title,
Items,
NestedValues = null
) {
const Container =
document.getElementById(
ContainerId
);
Container.replaceChildren();
const Heading =
document.createElement("h3");
Heading.textContent = Title;
Container.appendChild(Heading);
if (Items.length === 0) {
const Empty =
document.createElement("p");
Empty.className =
"AdminOverviewEmpty";
Empty.textContent = "None";
Container.appendChild(Empty);
return;
}
Container.appendChild(
CreateAdminOverviewIssueList(
Items,
NestedValues
)
);
}
function RenderAdminOverview() {
const CreatedPlants =
CollectAdminOverviewCreatedPlants();
const PlantEntries =
Object.entries(
AdminOverviewPlants
)
.filter(
([, Plant]) =>
Plant.Archived !== true
)
.sort(
([KeyA, PlantA], [KeyB, PlantB]) =>
Number(PlantA.Id) -
Number(PlantB.Id) ||
KeyA.localeCompare(KeyB)
);
const MissingSprites = [];
const NoWaysToObtain = [];
for (
const [PlantKey, Plant]
of PlantEntries
) {
const Missing =
GetAdminOverviewMissingSprites(
PlantKey,
Plant
);
if (Missing.length > 0) {
MissingSprites.push({
Label:
Plant.Name ?? PlantKey,
Missing: Missing
});
}
if (
Plant.Shop?.ShopPlant !== true &&
!CreatedPlants.has(PlantKey)
) {
NoWaysToObtain.push({
Label:
Plant.Name ?? PlantKey
});
}
}
const MutationEntries =
Object.entries(
AdminOverviewMutations
)
.filter(
([, Mutation]) =>
Mutation.Archived !== true
)
.sort(
([KeyA, MutationA], [KeyB, MutationB]) =>
Number(MutationA.Id) -
Number(MutationB.Id) ||
KeyA.localeCompare(KeyB)
);
const NoDescription = [];
const NoHint = [];
for (
const [MutationKey, Mutation]
of MutationEntries
) {
const Label =
Mutation.Name?.trim() ||
MutationKey;
if (
typeof Mutation.Description !==
"string" ||
Mutation.Description.trim() === ""
) {
NoDescription.push({
Label: Label
});
}
if (
typeof Mutation.Hint !==
"string" ||
Mutation.Hint.trim() === ""
) {
NoHint.push({
Label: Label
});
}
}
RenderAdminOverviewSection(
"AdminOverviewMissingSprites",
"Missing sprites",
MissingSprites,
Item => Item.Missing
);
RenderAdminOverviewSection(
"AdminOverviewNoObtain",
"No ways to obtain",
NoWaysToObtain
);
RenderAdminOverviewSection(
"AdminOverviewNoDescription",
"No description",
NoDescription
);
RenderAdminOverviewSection(
"AdminOverviewNoHint",
"No hint",
NoHint
);
}
async function StartAdminOverview() {
const Overview =
document.getElementById(
"AdminOverview"
);
if (Overview === null) {
return;
}
try {
const Status =
await GetAdminStatus();
if (
Status.Success !== true ||
Status.IsAdmin !== true
) {
return;
}
const [
PlantResult,
MutationResult
] = await Promise.all([
AdminOverviewRequest(
"AdminPlants.php"
),
AdminOverviewRequest(
"AdminMutations.php"
)
]);
AdminOverviewPlants =
PlantResult.Plants ?? {};
AdminOverviewMutations =
MutationResult.Mutations ?? {};
RenderAdminOverview();
Overview.hidden = false;
} catch (Error) {
console.error(
"Couldn't load admin overview:",
Error
);
const Message =
document.getElementById(
"AdminOverviewMessage"
);
Message.textContent =
Error.message ??
"Couldn't load admin overview.";
Overview.hidden = false;
}
}
document.addEventListener(
"DOMContentLoaded",
StartAdminOverview
);
+330
View File
@@ -71,6 +71,31 @@ function BindAdminPlantEditor() {
DuplicateAdminPlant
);
document.getElementById(
"AdminPlantImportButton"
).addEventListener(
"click",
() => {
document.getElementById(
"AdminPlantImportFile"
).click();
}
);
document.getElementById(
"AdminPlantImportFile"
).addEventListener(
"change",
ImportAdminPlantJson
);
document.getElementById(
"AdminPlantExportButton"
).addEventListener(
"click",
ExportAdminPlantJson
);
document.getElementById(
"AdminPlantForm"
).addEventListener(
@@ -930,6 +955,20 @@ async function SaveAdminPlant(
return;
}
try {
ValidateAdminPlant(
Plant,
AdminPlantCatalogue,
AdminPlantEditingKey
);
} catch (Error) {
SetAdminPlantMessage(
Error.message
);
return;
}
AdminPlantSavePending = true;
const SaveButton =
@@ -1833,6 +1872,297 @@ async function DeleteAdminPlantImage(
}
function GetAdminPlantPortableData(
Plant
) {
return {
PlantKey: Plant.PlantKey,
Name: Plant.Name,
Description: Plant.Description,
Tags: [...Plant.Tags],
GrowthTime: Plant.GrowthTime,
HarvestMultiplier:
Plant.HarvestMultiplier,
Effects: JSON.parse(
JSON.stringify(
Plant.Effects ?? {}
)
),
Shop: {
ShopPlant:
Plant.ShopPlant === true,
BaseCost:
Plant.ShopPlant === true
? Plant.BaseCost
: null
}
};
}
function ExportAdminPlantJson() {
const Form =
document.getElementById(
"AdminPlantForm"
);
if (!Form.reportValidity()) {
return;
}
try {
const Plant =
GetAdminPlantFormData();
ValidateAdminPlant(
Plant,
AdminPlantCatalogue,
AdminPlantEditingKey
);
DownloadAdminJson(
"Plant-" +
Plant.PlantKey +
".json",
{
Type: "Plant",
Version: 1,
Plant:
GetAdminPlantPortableData(
Plant
)
}
);
SetAdminPlantMessage(
"Plant JSON exported. Sprites and numeric ID are not included."
);
} catch (Error) {
SetAdminPlantMessage(
Error.message ??
"Couldn't export plant JSON."
);
}
}
async function ImportAdminPlantJson(
Event
) {
const Input = Event.target;
const File = Input.files?.[0];
Input.value = "";
if (File === undefined) {
return;
}
try {
const DocumentData =
await ReadAdminJsonFile(
File
);
if (
IsAdminPlainObject(
DocumentData
) &&
DocumentData.Type !==
undefined &&
DocumentData.Type !== "Plant"
) {
throw new Error(
"That JSON file is not a plant export."
);
}
if (
IsAdminPlainObject(
DocumentData
) &&
DocumentData.Version !==
undefined &&
Number(DocumentData.Version) !== 1
) {
throw new Error(
"Unsupported plant JSON version."
);
}
const Imported =
IsAdminPlainObject(
DocumentData?.Plant
)
? DocumentData.Plant
: DocumentData;
if (!IsAdminPlainObject(Imported)) {
throw new Error(
"Plant JSON must contain a plant object."
);
}
const PlantKey =
String(
Imported.PlantKey ?? ""
).trim();
const Existing =
AdminPlantCatalogue[
PlantKey
];
const Shop =
IsAdminPlainObject(
Imported.Shop
)
? Imported.Shop
: {};
const Plant = {
Id:
Existing?.Id ??
GetNextAdminPlantId(),
PlantKey: PlantKey,
Name:
String(
Imported.Name ?? ""
).trim(),
Description:
typeof Imported.Description ===
"string"
? Imported.Description.trim()
: "",
Tags:
Array.isArray(Imported.Tags)
? Imported.Tags.map(
Tag =>
String(Tag).trim()
)
: [],
GrowthTime:
Number(
Imported.GrowthTime
),
HarvestMultiplier:
Number(
Imported.HarvestMultiplier ??
1.5
),
Effects:
IsAdminPlainObject(
Imported.Effects
)
? JSON.parse(
JSON.stringify(
Imported.Effects
)
)
: {},
ShopPlant:
Shop.ShopPlant === true ||
Imported.ShopPlant === true,
BaseCost:
Shop.BaseCost ??
Imported.BaseCost ??
null,
IsNew:
Existing === undefined
};
ValidateAdminPlant(
Plant,
AdminPlantCatalogue,
Existing === undefined
? null
: PlantKey
);
if (Existing === undefined) {
StartNewAdminPlant();
AdminPlantEditingKey = null;
} else {
LoadAdminPlantIntoForm(
PlantKey
);
}
SetAdminPlantField(
"AdminPlantId",
Plant.Id
);
SetAdminPlantField(
"AdminPlantKey",
Plant.PlantKey
);
SetAdminPlantField(
"AdminPlantName",
Plant.Name
);
SetAdminPlantField(
"AdminPlantDescription",
Plant.Description
);
SetAdminPlantField(
"AdminPlantTags",
Plant.Tags.join(", ")
);
SetAdminPlantDurationFields(
Plant.GrowthTime
);
SetAdminPlantField(
"AdminPlantHarvestMultiplier",
Plant.HarvestMultiplier
);
document.getElementById(
"AdminPlantShopPlant"
).checked = Plant.ShopPlant;
SetAdminPlantField(
"AdminPlantBaseCost",
Plant.BaseCost ?? ""
);
SetAdminPlantField(
"AdminPlantEffects",
JSON.stringify(
Plant.Effects,
null,
4
)
);
document.getElementById(
"AdminPlantId"
).readOnly =
Existing !== undefined;
document.getElementById(
"AdminPlantKey"
).readOnly =
Existing !== undefined;
UpdateAdminPlantShopFields();
RenderAdminPlantPreview();
RenderAdminPlantImageManager();
SetAdminPlantMessage(
Existing === undefined
? "Plant JSON imported as a new plant. Review it, then save when ready."
: "Plant JSON imported over " +
PlantKey +
". Review it, then save when ready."
);
} catch (Error) {
SetAdminPlantMessage(
Error.message ??
"Couldn't import plant JSON."
);
}
}
function FormatAdminDuration(
Milliseconds
) {
+820
View File
@@ -0,0 +1,820 @@
function IsAdminPlainObject(Value) {
return (
Value !== null &&
typeof Value === "object" &&
!Array.isArray(Value)
);
}
function ValidateAdminInteger(
Value,
Name,
Minimum = null
) {
if (
!Number.isInteger(Value) ||
(
Minimum !== null &&
Value < Minimum
)
) {
throw new Error(
Name + " must be " +
(
Minimum === null
? "an integer."
: "an integer of at least " +
Minimum + "."
)
);
}
}
function ValidateAdminKey(
Value,
Name,
MaximumLength = 64
) {
if (
typeof Value !== "string" ||
Value.length === 0
) {
throw new Error(
Name + " cannot be empty."
);
}
if (
Value.length > MaximumLength ||
!/^[A-Za-z0-9_-]+$/.test(Value)
) {
throw new Error(
Name +
" may only contain letters, numbers, underscores and hyphens."
);
}
}
function ValidateAdminPlant(
Plant,
Catalogue,
EditingKey = null
) {
if (!IsAdminPlainObject(Plant)) {
throw new Error(
"Plant data is invalid."
);
}
ValidateAdminInteger(
Number(Plant.Id),
"ID",
1
);
const PlantKey =
String(
Plant.PlantKey ?? ""
).trim();
ValidateAdminKey(
PlantKey,
"Plant Key"
);
const ExistingAtId =
Object.entries(
Catalogue ?? {}
).find(
([, Candidate]) =>
Number(Candidate?.Id) ===
Number(Plant.Id)
);
if (
ExistingAtId !== undefined &&
ExistingAtId[0] !== PlantKey
) {
throw new Error(
"ID " + Plant.Id +
" already belongs to " +
ExistingAtId[0] + "."
);
}
if (
EditingKey !== null &&
EditingKey !== PlantKey
) {
throw new Error(
"An existing Plant Key cannot be changed."
);
}
if (
EditingKey !== null &&
Number(
Catalogue?.[EditingKey]?.Id
) !== Number(Plant.Id)
) {
throw new Error(
"An existing plant ID cannot be changed."
);
}
if (
EditingKey === null &&
Catalogue?.[PlantKey] !== undefined
) {
throw new Error(
"Plant Key " + PlantKey +
" already exists."
);
}
const Name =
String(
Plant.Name ?? ""
).trim();
if (
Name.length === 0 ||
Name.length > 64
) {
throw new Error(
"Name must contain between 1 and 64 characters."
);
}
if (
typeof Plant.Description !==
"string"
) {
throw new Error(
"Description must be text."
);
}
if (!Array.isArray(Plant.Tags)) {
throw new Error(
"Tags must be an array."
);
}
for (const Tag of Plant.Tags) {
if (
typeof Tag !== "string" ||
Tag.trim().length === 0 ||
Tag.trim().length > 64
) {
throw new Error(
"Tags contains an invalid tag."
);
}
}
ValidateAdminInteger(
Number(Plant.GrowthTime),
"Growth time",
0
);
const HarvestMultiplier =
Number(
Plant.HarvestMultiplier
);
if (
!Number.isFinite(
HarvestMultiplier
) ||
HarvestMultiplier < 0 ||
HarvestMultiplier > 9999.9999
) {
throw new Error(
"Harvest multiplier must be between 0 and 9999.9999."
);
}
if (!IsAdminPlainObject(Plant.Effects)) {
throw new Error(
"Effects JSON must be an object."
);
}
if (Plant.ShopPlant === true) {
ValidateAdminInteger(
Number(Plant.BaseCost),
"Base seed cost",
0
);
}
return true;
}
function GetAdminPlantKeyById(
PlantId,
PlantCatalogue
) {
return Object.entries(
PlantCatalogue ?? {}
).find(
([, Plant]) =>
Number(Plant?.Id) ===
Number(PlantId)
)?.[0] ?? null;
}
function ValidateAdminMutationMatrix(
Matrix,
Name,
Width = null,
Height = null
) {
if (
!Array.isArray(Matrix) ||
Matrix.length < 1 ||
Matrix.length > 7
) {
throw new Error(
Name +
" must contain between 1 and 7 rows."
);
}
const MatrixWidth =
Array.isArray(Matrix[0])
? Matrix[0].length
: 0;
if (
MatrixWidth < 1 ||
MatrixWidth > 7
) {
throw new Error(
Name +
" rows must contain between 1 and 7 cells."
);
}
for (const Row of Matrix) {
if (
!Array.isArray(Row) ||
Row.length !== MatrixWidth
) {
throw new Error(
Name +
" must be rectangular."
);
}
}
if (
Width !== null &&
MatrixWidth !== Width
) {
throw new Error(
Name +
" width must match the pattern."
);
}
if (
Height !== null &&
Matrix.length !== Height
) {
throw new Error(
Name +
" height must match the pattern."
);
}
return {
Width: MatrixWidth,
Height: Matrix.length
};
}
function ValidateAdminMutationTagList(
Value,
Name
) {
if (!Array.isArray(Value)) {
throw new Error(
Name + " must be an array."
);
}
for (const Tag of Value) {
if (
typeof Tag !== "string" ||
Tag.trim().length === 0 ||
Tag.trim().length > 64
) {
throw new Error(
Name +
" contains an invalid tag."
);
}
}
}
function ValidateAdminCaptureName(
Capture
) {
if (
typeof Capture !== "string" ||
!/^[A-Za-z][A-Za-z0-9_]*$/.test(
Capture
)
) {
throw new Error(
"Capture names must start with a letter and contain only letters, numbers and underscores."
);
}
}
function ValidateAdminMutationPatternCell(
Cell,
PlantCatalogue,
Captures
) {
if (
Cell === null ||
Cell === "Any" ||
Cell === "Empty"
) {
return;
}
if (typeof Cell === "string") {
if (
PlantCatalogue?.[Cell] ===
undefined
) {
throw new Error(
"Pattern references unknown plant: " +
Cell
);
}
return;
}
if (!IsAdminPlainObject(Cell)) {
throw new Error(
"Pattern contains an invalid matcher."
);
}
if (
Cell.Plant !== undefined &&
(
typeof Cell.Plant !==
"string" ||
PlantCatalogue?.[
Cell.Plant
] === undefined
)
) {
throw new Error(
"Pattern matcher references an unknown plant."
);
}
for (
const TagField
of [
"Tags",
"TagsAny",
"TagsNot"
]
) {
if (Cell[TagField] !== undefined) {
ValidateAdminMutationTagList(
Cell[TagField],
TagField
);
}
}
if (Cell.Capture !== undefined) {
ValidateAdminCaptureName(
Cell.Capture
);
Captures.add(
Cell.Capture
);
}
}
function ValidateAdminMutationResultCell(
Cell,
PlantCatalogue,
Captures
) {
if (
Cell === null ||
Cell === "Keep" ||
Cell === "Empty"
) {
return;
}
let PlantValue = null;
if (typeof Cell === "string") {
PlantValue = Cell;
} else if (
IsAdminPlainObject(Cell) &&
typeof Cell.Plant === "string"
) {
PlantValue = Cell.Plant;
}
if (PlantValue === null) {
throw new Error(
"Result contains an invalid value."
);
}
if (PlantValue.startsWith("$")) {
const Capture =
PlantValue.slice(1);
if (!Captures.has(Capture)) {
throw new Error(
"Result references unknown capture: " +
Capture
);
}
return;
}
if (
PlantCatalogue?.[
PlantValue
] === undefined
) {
throw new Error(
"Result references unknown plant: " +
PlantValue
);
}
}
function ValidateAdminMutationRelations(
Relations,
PlantCatalogue
) {
if (!IsAdminPlainObject(Relations)) {
throw new Error(
"Relations must be an object."
);
}
const ValidIds =
new Set(
Object.values(
PlantCatalogue ?? {}
).map(
Plant =>
Number(Plant.Id)
)
);
for (
const Name
of [
"PlantsUsed",
"PlantsCreated"
]
) {
const Values =
Relations[Name] ?? [];
if (!Array.isArray(Values)) {
throw new Error(
Name + " must be an array."
);
}
for (const PlantId of Values) {
if (
!Number.isInteger(
Number(PlantId)
) ||
!ValidIds.has(
Number(PlantId)
)
) {
throw new Error(
Name +
" references a plant that does not exist."
);
}
}
}
}
function ValidateAdminMutation(
Mutation,
MutationCatalogue,
PlantCatalogue,
EditingKey = null
) {
if (!IsAdminPlainObject(Mutation)) {
throw new Error(
"Mutation data is invalid."
);
}
ValidateAdminInteger(
Number(Mutation.Id),
"ID",
1
);
const MutationKey =
String(
Mutation.MutationKey ?? ""
).trim();
ValidateAdminKey(
MutationKey,
"Mutation Key"
);
const ExistingAtId =
Object.entries(
MutationCatalogue ?? {}
).find(
([, Candidate]) =>
Number(Candidate?.Id) ===
Number(Mutation.Id)
);
if (
ExistingAtId !== undefined &&
ExistingAtId[0] !== MutationKey
) {
throw new Error(
"ID " + Mutation.Id +
" already belongs to " +
ExistingAtId[0] + "."
);
}
if (
EditingKey !== null &&
EditingKey !== MutationKey
) {
throw new Error(
"An existing Mutation Key cannot be changed."
);
}
if (
EditingKey !== null &&
Number(
MutationCatalogue
?.[EditingKey]?.Id
) !== Number(Mutation.Id)
) {
throw new Error(
"An existing mutation ID cannot be changed."
);
}
if (
EditingKey === null &&
MutationCatalogue?.[
MutationKey
] !== undefined
) {
throw new Error(
"Mutation Key " +
MutationKey +
" already exists."
);
}
const Name =
String(
Mutation.Name ?? ""
).trim();
if (
Name.length === 0 ||
Name.length > 128
) {
throw new Error(
"Name must contain between 1 and 128 characters."
);
}
if (
typeof Mutation.Description !==
"string"
) {
throw new Error(
"Description must be text."
);
}
if (
Mutation.Hint !== null &&
Mutation.Hint !== undefined &&
typeof Mutation.Hint !== "string"
) {
throw new Error(
"Hint must be text."
);
}
ValidateAdminInteger(
Number(Mutation.Priority),
"Priority"
);
const Chance =
Number(
Mutation.Chance
);
if (
!Number.isFinite(Chance) ||
Chance < 0 ||
Chance > 1
) {
throw new Error(
"Chance must be between 0 and 100%."
);
}
ValidateAdminInteger(
Number(Mutation.Cooldown),
"Cooldown",
0
);
if (
Mutation.Rotation !== "None" &&
Mutation.Rotation !== "Any"
) {
throw new Error(
"Rotation must be None or Any."
);
}
const PatternSize =
ValidateAdminMutationMatrix(
Mutation.Pattern,
"Pattern"
);
const Captures =
new Set();
for (const Row of Mutation.Pattern) {
for (const Cell of Row) {
ValidateAdminMutationPatternCell(
Cell,
PlantCatalogue,
Captures
);
}
}
ValidateAdminMutationMatrix(
Mutation.Success,
"Success result",
PatternSize.Width,
PatternSize.Height
);
for (const Row of Mutation.Success) {
for (const Cell of Row) {
ValidateAdminMutationResultCell(
Cell,
PlantCatalogue,
Captures
);
}
}
if (
Mutation.Failure !== "Keep" &&
Mutation.Failure !== "Clear"
) {
ValidateAdminMutationMatrix(
Mutation.Failure,
"Failure result",
PatternSize.Width,
PatternSize.Height
);
for (const Row of Mutation.Failure) {
for (const Cell of Row) {
ValidateAdminMutationResultCell(
Cell,
PlantCatalogue,
Captures
);
}
}
}
ValidateAdminMutationRelations(
Mutation.Relations ?? {},
PlantCatalogue
);
return true;
}
function DownloadAdminJson(
FileName,
Data
) {
const BlobData =
new Blob(
[
JSON.stringify(
Data,
null,
4
) + "\n"
],
{
type: "application/json"
}
);
const Url =
URL.createObjectURL(
BlobData
);
const Link =
document.createElement("a");
Link.href = Url;
Link.download = FileName;
Link.click();
URL.revokeObjectURL(Url);
}
function ReadAdminJsonFile(File) {
return new Promise(
(Resolve, Reject) => {
const Reader =
new FileReader();
Reader.onload = () => {
try {
Resolve(
JSON.parse(
String(
Reader.result ?? ""
)
)
);
} catch (Error) {
Reject(
new Error(
"The selected file is not valid JSON."
)
);
}
};
Reader.onerror = () => {
Reject(
new Error(
"Couldn't read the selected JSON file."
)
);
};
Reader.readAsText(File);
}
);
}
+85
View File
@@ -2343,3 +2343,88 @@ input, button, textarea, select {
width: 100%;
}
}
.AdminEditorTransferActions {
margin: 0 0 1rem;
}
.AdminOverview,
.AdminMassSelectionPanel,
.AdminMassEditorForm {
margin-top: 1.5rem;
}
.AdminOverviewGroups {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
padding: 1rem;
}
.AdminOverviewGroups > section > h2 {
margin-bottom: 0.75rem;
font-size: 1.4rem;
}
.AdminOverviewGroups h3 {
margin: 1rem 0 0.35rem;
font-size: 1.1rem;
}
.AdminOverviewGroups h3:first-child {
margin-top: 0;
}
.AdminOverviewList,
.AdminOverviewList ul {
margin: 0;
padding-left: 1.25rem;
}
.AdminOverviewList ul {
color: var(--Subtext1);
}
.AdminOverviewEmpty {
margin: 0;
color: var(--Overlay2);
}
.AdminOverview > .PageMessage:empty {
display: none;
}
.AdminMassSelectionBody {
padding: 1rem;
}
.AdminMassChecklist {
max-height: 20rem;
}
.AdminMassField {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.75rem;
align-items: start;
}
.AdminMassApplyField {
align-self: start;
margin-top: 1.55rem;
}
.AdminMassField :disabled {
opacity: 0.55;
}
@media (max-width: 700px) {
.AdminOverviewGroups,
.AdminMassField {
grid-template-columns: 1fr;
}
.AdminMassApplyField {
margin-top: 0;
}
}