let AdminTransferPlants = {}; let AdminTransferMutations = {}; let AdminTransferImportItems = []; let AdminTransferExportSelection = new Set(); let AdminTransferExportSearch = ""; let AdminTransferSavePending = false; async function StartAdminContentTransfer() { const ImportButton = document.getElementById( "AdminMassImportButton" ); if (ImportButton === null) { return; } try { const Status = await GetAdminStatus(); if ( Status.Success !== true || Status.IsAdmin !== true ) { return; } BindAdminContentTransfer(); await LoadAdminTransferCatalogues(); } catch (Error) { console.error( "Couldn't start Admin content transfer:", Error ); SetAdminTransferImportMessage( Error.message ?? "Couldn't load mass import and export." ); } } function BindAdminContentTransfer() { document.getElementById( "AdminMassImportButton" ).addEventListener( "click", () => { document.getElementById( "AdminMassImportFiles" ).click(); } ); document.getElementById( "AdminMassImportChooseButton" ).addEventListener( "click", () => { document.getElementById( "AdminMassImportFiles" ).click(); } ); document.getElementById( "AdminMassImportFiles" ).addEventListener( "change", ImportAdminContentFiles ); document.getElementById( "AdminMassImportSaveButton" ).addEventListener( "click", SaveAdminImportedContent ); document.getElementById( "AdminMassImportCloseButton" ).addEventListener( "click", () => HideAdminTransferPanel( "AdminMassImportPanel" ) ); document.getElementById( "AdminMassExportButton" ).addEventListener( "click", ShowAdminMassExport ); document.getElementById( "AdminMassExportCloseButton" ).addEventListener( "click", () => HideAdminTransferPanel( "AdminMassExportPanel" ) ); document.getElementById( "AdminMassExportSearchInput" ).addEventListener( "input", Event => { AdminTransferExportSearch = Event.target.value; RenderAdminMassExport(); } ); document.getElementById( "AdminMassExportSelectAllButton" ).addEventListener( "click", () => SelectAdminTransferExport( "All" ) ); document.getElementById( "AdminMassExportSelectPlantsButton" ).addEventListener( "click", () => SelectAdminTransferExport( "Plant" ) ); document.getElementById( "AdminMassExportSelectMutationsButton" ).addEventListener( "click", () => SelectAdminTransferExport( "Mutation" ) ); document.getElementById( "AdminMassExportDeselectAllButton" ).addEventListener( "click", () => { AdminTransferExportSelection.clear(); RenderAdminMassExport(); } ); document.getElementById( "AdminMassExportSaveButton" ).addEventListener( "click", ExportAdminSelectedContent ); } async function AdminTransferRequest( Endpoint, Action = "List", Data = {} ) { 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, ...Data }) } ); let Result; try { Result = await Response.json(); } catch (Error) { throw new Error( "Admin API returned invalid JSON." ); } if ( !Response.ok || Result.Success !== true ) { throw new Error( Result.Error ?? "Admin request failed." ); } return Result; } async function LoadAdminTransferCatalogues() { const [ PlantResult, MutationResult ] = await Promise.all([ AdminTransferRequest( "AdminPlants.php" ), AdminTransferRequest( "AdminMutations.php" ) ]); AdminTransferPlants = PlantResult.Plants ?? {}; AdminTransferMutations = MutationResult.Mutations ?? {}; Plants = AdminTransferPlants; MutationSets = AdminTransferMutations; } function ShowAdminTransferPanel( PanelId ) { for ( const Id of [ "AdminMassImportPanel", "AdminMassExportPanel" ] ) { const Panel = document.getElementById(Id); if (Panel !== null) { Panel.hidden = Id !== PanelId; } } document.getElementById( PanelId )?.scrollIntoView({ behavior: "smooth", block: "start" }); } function HideAdminTransferPanel( PanelId ) { const Panel = document.getElementById( PanelId ); if (Panel !== null) { Panel.hidden = true; } } async function ImportAdminContentFiles( Event ) { const Input = Event.target; const Files = [ ...(Input.files ?? []) ]; Input.value = ""; if (Files.length === 0) { return; } ShowAdminTransferPanel( "AdminMassImportPanel" ); SetAdminTransferImportMessage( "Reading " + Files.length.toLocaleString() + (Files.length === 1 ? " file..." : " files...") ); AdminTransferImportItems = []; for (const File of Files) { AdminTransferImportItems.push( await ParseAdminTransferFile( File ) ); } PrepareAdminTransferImportItems(); RenderAdminMassImport(); } async function ParseAdminTransferFile( File ) { try { const DocumentData = await ReadAdminJsonFile( File ); if (!IsAdminPlainObject( DocumentData )) { throw new Error( "JSON must contain an object." ); } let Type = typeof DocumentData.Type === "string" ? DocumentData.Type .trim() : ""; if (Type === "") { if ( IsAdminPlainObject( DocumentData.Plant ) || DocumentData.PlantKey !== undefined ) { Type = "Plant"; } else if ( IsAdminPlainObject( DocumentData.Mutation ) || DocumentData.MutationKey !== undefined ) { Type = "Mutation"; } } if ( Type !== "Plant" && Type !== "Mutation" ) { throw new Error( "JSON must be a Plant or Mutation export." ); } if ( DocumentData.Version !== undefined && Number( DocumentData.Version ) !== 1 ) { throw new Error( "Unsupported " + Type.toLowerCase() + " JSON version." ); } const Imported = IsAdminPlainObject( DocumentData[Type] ) ? DocumentData[Type] : DocumentData; if (!IsAdminPlainObject( Imported )) { throw new Error( Type + " JSON must contain a " + Type.toLowerCase() + " object." ); } return { FileName: File.name, Type: Type, Raw: Imported, Key: String( Imported[ Type === "Plant" ? "PlantKey" : "MutationKey" ] ?? "" ).trim(), Data: null, Existing: false, Error: null, Saved: false, SaveError: null }; } catch (Error) { return { FileName: File.name, Type: null, Raw: null, Key: "", Data: null, Existing: false, Error: Error.message ?? "Couldn't read JSON file.", Saved: false, SaveError: null }; } } function PrepareAdminTransferImportItems() { const DuplicateCounts = new Map(); for ( const Item of AdminTransferImportItems ) { if ( Item.Error !== null || Item.Type === null || Item.Key === "" ) { continue; } const DuplicateKey = Item.Type + ":" + Item.Key; DuplicateCounts.set( DuplicateKey, ( DuplicateCounts.get( DuplicateKey ) ?? 0 ) + 1 ); } for ( const Item of AdminTransferImportItems ) { if ( Item.Error !== null || Item.Type === null ) { continue; } if (Item.Key === "") { Item.Error = Item.Type + " Key cannot be empty."; continue; } if ( DuplicateCounts.get( Item.Type + ":" + Item.Key ) > 1 ) { Item.Error = "The batch contains more than one " + Item.Type.toLowerCase() + " with key " + Item.Key + "."; } } const PlantCatalogue = CloneAdminTransferValue( AdminTransferPlants ); let NextPlantId = GetNextAdminTransferId( AdminTransferPlants ); for ( const Item of AdminTransferImportItems ) { if ( Item.Type !== "Plant" || Item.Error !== null ) { continue; } const Existing = AdminTransferPlants[ Item.Key ]; Item.Existing = Existing !== undefined; try { const Plant = NormalizeAdminTransferPlant( Item.Raw, Item.Key, Existing, Existing?.Id ?? NextPlantId ); ValidateAdminPlant( Plant, PlantCatalogue, Item.Existing ? Item.Key : null ); Item.Data = Plant; PlantCatalogue[ Item.Key ] = CloneAdminTransferValue( Plant ); if (!Item.Existing) { NextPlantId++; } } catch (Error) { Item.Error = Error.message ?? "Plant validation failed."; } } const MutationCatalogue = CloneAdminTransferValue( AdminTransferMutations ); let NextMutationId = GetNextAdminTransferId( AdminTransferMutations ); for ( const Item of AdminTransferImportItems ) { if ( Item.Type !== "Mutation" || Item.Error !== null ) { continue; } const Existing = AdminTransferMutations[ Item.Key ]; Item.Existing = Existing !== undefined; try { const Mutation = NormalizeAdminTransferMutation( Item.Raw, Item.Key, Existing, Existing?.Id ?? NextMutationId, PlantCatalogue ); ValidateAdminMutation( Mutation, MutationCatalogue, PlantCatalogue, Item.Existing ? Item.Key : null ); Item.Data = Mutation; MutationCatalogue[ Item.Key ] = CloneAdminTransferValue( Mutation ); if (!Item.Existing) { NextMutationId++; } } catch (Error) { Item.Error = Error.message ?? "Mutation validation failed."; } } } function NormalizeAdminTransferPlant( Imported, PlantKey, Existing, PlantId ) { const Shop = IsAdminPlainObject( Imported.Shop ) ? Imported.Shop : {}; const ShopPlant = Shop.ShopPlant === true || Imported.ShopPlant === true; return { Id: Number(PlantId), 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 ) ? CloneAdminTransferValue( Imported.Effects ) : {}, DirectionalSprites: Imported.DirectionalSprites === true, ShopPlant: ShopPlant, BaseCost: ShopPlant ? Number( Shop.BaseCost ?? Imported.BaseCost ) : null, IsNew: Existing === undefined }; } function NormalizeAdminTransferMutation( Imported, MutationKey, Existing, MutationId, PlantCatalogue ) { return { Id: Number(MutationId), 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, Lists: CloneAdminTransferValue( Imported.Lists ?? [] ), Pattern: CloneAdminTransferValue( Imported.Pattern ), Success: CloneAdminTransferValue( Imported.Success ), Failure: CloneAdminTransferValue( Imported.Failure ?? "Keep" ), Relations: ConvertAdminTransferRelations( Imported.Relations, PlantCatalogue ), IsNew: Existing === undefined }; } function ConvertAdminTransferRelations( Relations, PlantCatalogue ) { 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 = PlantCatalogue[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; } function GetNextAdminTransferId( Catalogue ) { return Math.max( 0, ...Object.values( Catalogue ?? {} ).map( Item => Number( Item?.Id ?? 0 ) ) ) + 1; } function GetAdminTransferPreviewCatalogues() { const PreviewPlants = CloneAdminTransferValue( AdminTransferPlants ); const PreviewMutations = CloneAdminTransferValue( AdminTransferMutations ); for ( const Item of AdminTransferImportItems ) { if ( Item.Error !== null || Item.Data === null || Item.Saved === true ) { continue; } if (Item.Type === "Plant") { PreviewPlants[Item.Key] = GetAdminTransferDisplayPlant( Item.Key, Item.Data, AdminTransferPlants[ Item.Key ] ); } else if ( Item.Type === "Mutation" ) { PreviewMutations[Item.Key] = GetAdminTransferDisplayMutation( Item.Key, Item.Data ); } } for ( const [PlantKey, Plant] of Object.entries( PreviewPlants ) ) { PreviewPlants[PlantKey] = GetAdminTransferDisplayPlant( PlantKey, Plant, AdminTransferPlants[ PlantKey ] ); } return { Plants: PreviewPlants, Mutations: PreviewMutations }; } function GetAdminTransferDisplayPlant( PlantKey, Plant, Existing = null ) { const Shop = IsAdminPlainObject( Plant.Shop ) ? Plant.Shop : {}; const ShopPlant = Plant.ShopPlant === true || Shop.ShopPlant === true; return { ...Plant, PlantKey: PlantKey, Shop: { ShopPlant: ShopPlant, BaseCost: ShopPlant ? ( Plant.BaseCost ?? Shop.BaseCost ?? null ) : null }, ImageStages: Plant.ImageStages ?? Existing?.ImageStages ?? [], DirectionalImageStages: Plant.DirectionalImageStages ?? Existing ?.DirectionalImageStages ?? [] }; } function GetAdminTransferDisplayMutation( MutationKey, Mutation ) { return { ...Mutation, MutationKey: MutationKey }; } function RenderAdminMassImport() { const List = document.getElementById( "AdminMassImportList" ); List.replaceChildren(); const Preview = GetAdminTransferPreviewCatalogues(); Plants = Preview.Plants; MutationSets = Preview.Mutations; for ( const Item of AdminTransferImportItems ) { if ( Item.Error !== null || Item.Data === null ) { List.appendChild( CreateAdminTransferErrorCard( Item ) ); continue; } let Card; if (Item.Type === "Plant") { Card = CreateAdminTransferPlantCard( Item.Key, Preview.Plants[ Item.Key ], Preview.Plants, Preview.Mutations ); } else { Card = CreateAdminTransferMutationCard( Item.Key, Preview.Mutations[ Item.Key ], Preview.Plants ); } AddAdminTransferCardStatus( Card, Item.Saved ? "Saved" : Item.Existing ? "Update" : "New", Item.FileName, Item.SaveError ); if (Item.SaveError !== null) { Card.classList.add( "AdminTransferCardInvalid" ); } List.appendChild(Card); } const ReadyPlants = AdminTransferImportItems.filter( Item => Item.Type === "Plant" && Item.Error === null && Item.Data !== null && Item.Saved !== true ).length; const ReadyMutations = AdminTransferImportItems.filter( Item => Item.Type === "Mutation" && Item.Error === null && Item.Data !== null && Item.Saved !== true ).length; const Valid = ReadyPlants + ReadyMutations; const Invalid = AdminTransferImportItems.filter( Item => Item.Error !== null || Item.SaveError !== null ).length; const SavedPlants = AdminTransferImportItems.filter( Item => Item.Type === "Plant" && Item.Saved === true ).length; const SavedMutations = AdminTransferImportItems.filter( Item => Item.Type === "Mutation" && Item.Saved === true ).length; const Saved = SavedPlants + SavedMutations; const SummaryParts = []; if (ReadyPlants > 0) { SummaryParts.push( ReadyPlants.toLocaleString() + (ReadyPlants === 1 ? " plant ready" : " plants ready") ); } if (ReadyMutations > 0) { SummaryParts.push( ReadyMutations.toLocaleString() + (ReadyMutations === 1 ? " mutation ready" : " mutations ready") ); } if (SavedPlants > 0) { SummaryParts.push( SavedPlants.toLocaleString() + (SavedPlants === 1 ? " plant saved" : " plants saved") ); } if (SavedMutations > 0) { SummaryParts.push( SavedMutations.toLocaleString() + (SavedMutations === 1 ? " mutation saved" : " mutations saved") ); } if (Invalid > 0) { SummaryParts.push( Invalid.toLocaleString() + " invalid" ); } document.getElementById( "AdminMassImportSummary" ).textContent = SummaryParts.join(" · ") || "No files selected"; const SaveButton = document.getElementById( "AdminMassImportSaveButton" ); SaveButton.disabled = AdminTransferSavePending || Valid === 0; if ( AdminTransferImportItems.length === 0 ) { SetAdminTransferImportMessage( "Choose JSON files to review them here." ); } else if (Invalid > 0) { SetAdminTransferImportMessage( "Invalid files are skipped. Valid files can still be saved." ); } else if (Saved === 0) { SetAdminTransferImportMessage( "Review the imported content, then save when ready." ); } } function CreateAdminTransferErrorCard( Item ) { const Card = document.createElement( "article" ); Card.className = "Panel AdminTransferErrorCard"; const Header = document.createElement( "div" ); Header.className = "PanelHeader AdminTransferErrorHeader"; const Title = document.createElement( "h2" ); Title.textContent = Item.Key || Item.FileName; const Status = document.createElement( "span" ); Status.className = "AdminTransferStatus AdminTransferStatusError"; Status.textContent = "Invalid"; Header.append( Title, Status ); const Body = document.createElement( "div" ); Body.className = "AdminTransferErrorBody"; const FileName = document.createElement( "p" ); FileName.className = "AdminTransferFileName"; FileName.textContent = Item.FileName; const ErrorText = document.createElement( "p" ); ErrorText.textContent = Item.Error ?? Item.SaveError ?? "Unknown import error."; Body.append( FileName, ErrorText ); Card.append( Header, Body ); return Card; } async function SaveAdminImportedContent() { if (AdminTransferSavePending) { return; } const PlantsToSave = AdminTransferImportItems.filter( Item => Item.Type === "Plant" && Item.Error === null && Item.Data !== null && Item.Saved !== true ); const MutationsToSave = AdminTransferImportItems.filter( Item => Item.Type === "Mutation" && Item.Error === null && Item.Data !== null && Item.Saved !== true ); if ( PlantsToSave.length === 0 && MutationsToSave.length === 0 ) { return; } AdminTransferSavePending = true; const SaveButton = document.getElementById( "AdminMassImportSaveButton" ); SaveButton.disabled = true; SaveButton.textContent = "Saving..."; SetAdminTransferImportMessage( "Saving imported content..." ); let SavedCount = 0; let FailedCount = 0; for (const Item of PlantsToSave) { try { await AdminTransferRequest( "AdminPlants.php", "Save", { Plant: Item.Data } ); Item.Saved = true; Item.SaveError = null; SavedCount++; } catch (Error) { Item.SaveError = Error.message ?? "Couldn't save plant."; FailedCount++; } } for (const Item of MutationsToSave) { try { await AdminTransferRequest( "AdminMutations.php", "Save", { Mutation: Item.Data } ); Item.Saved = true; Item.SaveError = null; SavedCount++; } catch (Error) { Item.SaveError = Error.message ?? "Couldn't save mutation."; FailedCount++; } } try { localStorage.removeItem( "SarahtoninGardenContent" ); await LoadAdminTransferCatalogues(); if ( typeof StartAdminOverview === "function" ) { await StartAdminOverview(); } } catch (Error) { console.error( "Couldn't refresh Admin content after import:", Error ); } AdminTransferSavePending = false; SaveButton.textContent = "Save imported content"; RenderAdminMassImport(); if (FailedCount === 0) { SetAdminTransferImportMessage( SavedCount.toLocaleString() + (SavedCount === 1 ? " item saved." : " items saved.") ); } else { SetAdminTransferImportMessage( SavedCount.toLocaleString() + " saved, " + FailedCount.toLocaleString() + " failed. Failed items are shown above." ); } } function ShowAdminMassExport() { AdminTransferExportSearch = ""; const SearchInput = document.getElementById( "AdminMassExportSearchInput" ); SearchInput.value = ""; ShowAdminTransferPanel( "AdminMassExportPanel" ); RenderAdminMassExport(); } function RenderAdminMassExport() { const List = document.getElementById( "AdminMassExportList" ); List.replaceChildren(); Plants = AdminTransferPlants; MutationSets = AdminTransferMutations; const Query = AdminTransferExportSearch .trim() .toLocaleLowerCase(); const Items = GetAdminTransferExportItems() .filter( Item => DoesAdminTransferItemMatch( Item, Query ) ); for (const Item of Items) { let Card; if (Item.Type === "Plant") { Card = CreateAdminTransferPlantCard( Item.Key, Item.Data, AdminTransferPlants, AdminTransferMutations ); } else { Card = CreateAdminTransferMutationCard( Item.Key, Item.Data, AdminTransferPlants ); } DecorateAdminTransferExportCard( Card, Item ); List.appendChild(Card); } const PlantCount = [ ...AdminTransferExportSelection ].filter( Key => Key.startsWith( "Plant:" ) ).length; const MutationCount = [ ...AdminTransferExportSelection ].filter( Key => Key.startsWith( "Mutation:" ) ).length; document.getElementById( "AdminMassExportSummary" ).textContent = PlantCount.toLocaleString() + (PlantCount === 1 ? " plant" : " plants") + " · " + MutationCount.toLocaleString() + (MutationCount === 1 ? " mutation" : " mutations"); document.getElementById( "AdminMassExportSaveButton" ).disabled = AdminTransferExportSelection .size === 0; SetAdminTransferExportMessage( Items.length === 0 ? "No plants or mutations match your search." : AdminTransferExportSelection.size === 0 ? "Click cards to select content for export." : AdminTransferExportSelection.size .toLocaleString() + " selected." ); } function GetAdminTransferExportItems() { const Items = []; for ( const [PlantKey, Plant] of Object.entries( AdminTransferPlants ).sort( (A, B) => Number(A[1].Id) - Number(B[1].Id) || A[0].localeCompare(B[0]) ) ) { Items.push({ Type: "Plant", Key: PlantKey, Data: Plant }); } for ( const [MutationKey, Mutation] of Object.entries( AdminTransferMutations ).sort( (A, B) => Number(A[1].Id) - Number(B[1].Id) || A[0].localeCompare(B[0]) ) ) { Items.push({ Type: "Mutation", Key: MutationKey, Data: Mutation }); } return Items; } function DoesAdminTransferItemMatch( Item, Query ) { if (Query === "") { return true; } const SearchParts = [ Item.Type, Item.Key, Item.Data.Id, Item.Data.Name, Item.Data.Description, Item.Data.Hint, ...(Array.isArray( Item.Data.Tags ) ? Item.Data.Tags : []) ]; return SearchParts .filter( Value => Value !== undefined && Value !== null ) .join(" ") .toLocaleLowerCase() .includes(Query); } function DecorateAdminTransferExportCard( Card, Item ) { const SelectionKey = Item.Type + ":" + Item.Key; const Header = Card.querySelector( ".PanelHeader" ); const Toggle = document.createElement( "label" ); Toggle.className = "AdminTransferSelectToggle"; const Checkbox = document.createElement( "input" ); Checkbox.type = "checkbox"; Checkbox.checked = AdminTransferExportSelection.has( SelectionKey ); const Label = document.createElement( "span" ); Label.textContent = Item.Type; Toggle.append( Checkbox, Label ); Header.appendChild(Toggle); Card.classList.add( "AdminTransferSelectableCard" ); Card.dataset.selected = String(Checkbox.checked); Checkbox.addEventListener( "change", () => { SetAdminTransferExportSelection( SelectionKey, Checkbox.checked ); } ); Card.addEventListener( "click", Event => { if ( Event.target.closest( "input, label, a, button" ) !== null ) { return; } Checkbox.checked = !Checkbox.checked; SetAdminTransferExportSelection( SelectionKey, Checkbox.checked ); } ); } function SetAdminTransferExportSelection( SelectionKey, Selected ) { if (Selected) { AdminTransferExportSelection.add( SelectionKey ); } else { AdminTransferExportSelection.delete( SelectionKey ); } RenderAdminMassExport(); } function SelectAdminTransferExport( Type ) { for ( const Item of GetAdminTransferExportItems() ) { if ( Type !== "All" && Item.Type !== Type ) { continue; } AdminTransferExportSelection.add( Item.Type + ":" + Item.Key ); } RenderAdminMassExport(); } function ExportAdminSelectedContent() { const Files = []; for ( const SelectionKey of AdminTransferExportSelection ) { const Separator = SelectionKey.indexOf(":"); const Type = SelectionKey.slice( 0, Separator ); const Key = SelectionKey.slice( Separator + 1 ); if (Type === "Plant") { const Plant = AdminTransferPlants[Key]; if (Plant === undefined) { continue; } Files.push({ Name: "Plant-" + Key + ".json", Text: JSON.stringify( { Type: "Plant", Version: 1, Plant: GetAdminTransferPortablePlant( Key, Plant ) }, null, 4 ) + "\n" }); continue; } if (Type === "Mutation") { const Mutation = AdminTransferMutations[ Key ]; if (Mutation === undefined) { continue; } Files.push({ Name: "Mutation-" + Key + ".json", Text: JSON.stringify( { Type: "Mutation", Version: 1, Mutation: GetAdminTransferPortableMutation( Key, Mutation ) }, null, 4 ) + "\n" }); } } if (Files.length === 0) { SetAdminTransferExportMessage( "Select at least one plant or mutation." ); return; } DownloadAdminTransferZip( "Garden-Content-Export.zip", Files ); SetAdminTransferExportMessage( Files.length.toLocaleString() + (Files.length === 1 ? " JSON file exported." : " JSON files exported.") ); } function GetAdminTransferPortablePlant( PlantKey, Plant ) { return { PlantKey: PlantKey, Name: Plant.Name, Description: Plant.Description ?? "", Tags: Array.isArray(Plant.Tags) ? [...Plant.Tags] : [], GrowthTime: Number(Plant.GrowthTime), HarvestMultiplier: Number( Plant.HarvestMultiplier ?? 1.5 ), Effects: CloneAdminTransferValue( Plant.Effects ?? {} ), DirectionalSprites: Plant.DirectionalSprites === true, Shop: { ShopPlant: Plant.Shop?.ShopPlant === true, BaseCost: Plant.Shop?.ShopPlant === true ? Plant.Shop.BaseCost : null } }; } function GetAdminTransferPortableMutation( MutationKey, Mutation ) { return { MutationKey: MutationKey, Name: Mutation.Name, Description: Mutation.Description ?? "", Hint: typeof Mutation.Hint === "string" && Mutation.Hint.length > 0 ? Mutation.Hint : null, Priority: Number( Mutation.Priority ?? 0 ), Chance: Number( Mutation.Chance ), Cooldown: Number( Mutation.Cooldown ), Rotation: Mutation.Rotation ?? "None", AllowImmature: Mutation.AllowImmature === true, Lists: CloneAdminTransferValue( Mutation.Lists ?? [] ), Pattern: CloneAdminTransferValue( Mutation.Pattern ), Success: CloneAdminTransferValue( Mutation.Success ), Failure: CloneAdminTransferValue( Mutation.Failure ?? "Keep" ), Relations: GetAdminTransferPortableRelations( Mutation.Relations ) }; } function GetAdminTransferPortableRelations( Relations ) { const Result = { PlantsUsed: [], PlantsCreated: [] }; for ( const RelationName of [ "PlantsUsed", "PlantsCreated" ] ) { for ( const PlantId of Relations?.[ RelationName ] ?? [] ) { const PlantKey = GetAdminTransferPlantKeyById( PlantId, AdminTransferPlants ); if (PlantKey !== null) { Result[RelationName].push( PlantKey ); } } } return Result; } function GetAdminTransferPlantKeyById( PlantId, PlantCatalogue ) { return Object.entries( PlantCatalogue ?? {} ).find( ([, Plant]) => Number(Plant.Id) === Number(PlantId) )?.[0] ?? null; } function CreateAdminTransferPlantCard( PlantKey, Plant, PlantCatalogue, MutationCatalogue ) { const Card = document.createElement( "article" ); Card.className = "Panel PlantEncyclopediaCard AdminTransferCard"; const Header = document.createElement( "header" ); Header.className = "PanelHeader PlantEncyclopediaHeader AdminTransferCardHeader"; const NumberElement = document.createElement( "span" ); NumberElement.className = "PlantEncyclopediaNumber"; NumberElement.textContent = String( Plant.Id ).padStart( 3, "0" ); const NameElement = document.createElement( "h2" ); NameElement.textContent = Plant.Name; Header.append( NumberElement, NameElement ); const Body = document.createElement( "div" ); Body.className = "PlantEncyclopediaBody"; Body.append( CreateAdminTransferPlantVisual( PlantKey, Plant ), CreateAdminTransferPlantDetails( Plant, PlantCatalogue, MutationCatalogue ), CreateAdminTransferPlantRelations( Plant.Id, MutationCatalogue ) ); Card.append( Header, Body ); return Card; } function CreateAdminTransferPlantVisual( PlantKey, Plant ) { const Visual = document.createElement( "div" ); Visual.className = "PlantTile PlantEncyclopediaVisual"; const MatureImage = GetPlantMatureImageSource( PlantKey ); if (MatureImage === null) { const Missing = document.createElement( "span" ); Missing.className = "PlantEncyclopediaMissing"; Missing.textContent = "No image"; Visual.appendChild(Missing); return Visual; } const Image = document.createElement( "img" ); Image.className = "PlantSprite"; Image.src = MatureImage; Image.alt = Plant.Name; Visual.appendChild(Image); return Visual; } function CreateAdminTransferPlantDetails( Plant, PlantCatalogue, MutationCatalogue ) { const Details = document.createElement( "div" ); Details.className = "PlantEncyclopediaDetails"; const Description = document.createElement( "p" ); Description.className = "PlantEncyclopediaDescription"; Description.textContent = Plant.Description || "No description."; Details.appendChild(Description); const Tags = Array.isArray(Plant.Tags) ? Plant.Tags : []; const Cost = GetCataloguePlantCostInfo( Plant.Id, PlantCatalogue, MutationCatalogue )?.Cost ?? null; const Multiplier = Number( Plant.HarvestMultiplier ?? 1.5 ); const Reward = Cost === null ? null : Math.ceil( Cost * Multiplier ); const GrowthTime = Number( Plant.GrowthTime ); const Dph = Cost === null || Reward === null || !Number.isFinite(GrowthTime) || GrowthTime <= 0 ? null : (Reward - Cost) * 3600000 / GrowthTime; Details.append( CreateAdminTransferStat( "Plant Key", Plant.PlantKey ?? "" ), CreateAdminTransferStat( "Tags", Tags.length === 0 ? "None" : Tags.join(", ") ), CreateAdminTransferStat( "Growth", FormatAdminTransferDuration( GrowthTime ) ), CreateAdminTransferStat( "Cost", Cost === null ? "Unavailable" : Cost.toLocaleString() + " Dew" ), CreateAdminTransferStat( "Reward", Reward === null ? "Unavailable" : Reward.toLocaleString() + " Dew" ), CreateAdminTransferStat( "Dew / hour", Dph === null ? "Unavailable" : Dph.toLocaleString( undefined, { maximumFractionDigits: 2 } ) ) ); return Details; } function CreateAdminTransferStat( Name, Value ) { const Row = document.createElement( "div" ); Row.className = "PlantEncyclopediaStat"; const NameElement = document.createElement( "span" ); NameElement.className = "PlantEncyclopediaStatName"; NameElement.textContent = Name; const ValueElement = document.createElement( "span" ); ValueElement.textContent = Value; Row.append( NameElement, ValueElement ); return Row; } function CreateAdminTransferPlantRelations( PlantId, MutationCatalogue ) { const Relations = document.createElement( "div" ); Relations.className = "PlantEncyclopediaRelations"; const CreatedBy = []; const UsedIn = []; for ( const [MutationKey, Mutation] of Object.entries( MutationCatalogue ) ) { if ( Mutation.Relations ?.PlantsCreated ?.includes( Number(PlantId) ) ) { CreatedBy.push({ Key: MutationKey, Name: Mutation.Name ?? MutationKey }); } if ( Mutation.Relations ?.PlantsUsed ?.includes( Number(PlantId) ) ) { UsedIn.push({ Key: MutationKey, Name: Mutation.Name ?? MutationKey }); } } Relations.append( CreateAdminTransferRelationGroup( "Created by", CreatedBy ), CreateAdminTransferRelationGroup( "Used in", UsedIn ) ); return Relations; } function CreateAdminTransferRelationGroup( Heading, Entries ) { const Group = document.createElement( "section" ); Group.className = "PlantEncyclopediaRelationGroup"; const Title = document.createElement( "h3" ); Title.textContent = Heading; const List = document.createElement( "ul" ); List.className = "PlantEncyclopediaRelationList"; if (Entries.length === 0) { const Empty = document.createElement( "li" ); Empty.className = "PlantEncyclopediaRelationEmpty"; Empty.textContent = "None"; List.appendChild(Empty); } else { for ( const Entry of Entries.sort( (A, B) => A.Name.localeCompare( B.Name ) ) ) { const Item = document.createElement( "li" ); Item.textContent = Entry.Name; List.appendChild(Item); } } Group.append( Title, List ); return Group; } function CreateAdminTransferMutationCard( MutationKey, Mutation, PlantCatalogue ) { const Card = document.createElement( "article" ); Card.className = "Panel MutationEncyclopediaCard AdminTransferCard"; const Header = document.createElement( "header" ); Header.className = "PanelHeader MutationEncyclopediaHeader AdminTransferCardHeader"; const NumberElement = document.createElement( "span" ); NumberElement.className = "MutationEncyclopediaNumber"; NumberElement.textContent = String( Mutation.Id ).padStart( 3, "0" ); const NameElement = document.createElement( "h2" ); NameElement.textContent = Mutation.Name; Header.append( NumberElement, NameElement ); const Body = document.createElement( "div" ); Body.className = "MutationEncyclopediaBody"; const Description = document.createElement( "p" ); Description.className = "MutationEncyclopediaDescription"; Description.textContent = Mutation.Description || "No description."; Body.append( Description, CreateAdminTransferMutationStats( MutationKey, Mutation ), CreateAdminTransferMutationRecipe( Mutation, PlantCatalogue ), CreateAdminTransferMutationRelations( Mutation, PlantCatalogue ) ); Card.append( Header, Body ); return Card; } function CreateAdminTransferMutationStats( MutationKey, Mutation ) { const Stats = document.createElement( "div" ); Stats.className = "MutationEncyclopediaStats"; Stats.append( CreateAdminTransferStat( "Mutation Key", MutationKey ), CreateAdminTransferStat( "Chance", FormatAdminTransferChance( Mutation.Chance ) ), CreateAdminTransferStat( "Cooldown", FormatAdminTransferDuration( Mutation.Cooldown, true ) ), CreateAdminTransferStat( "Rotation", Mutation.Rotation ?? "None" ), CreateAdminTransferStat( "Mature plants", Mutation.AllowImmature === true ? "Not required" : "Required" ) ); return Stats; } function CreateAdminTransferMutationRecipe( Mutation, PlantCatalogue ) { const Section = document.createElement( "section" ); Section.className = "MutationEncyclopediaRecipeSection"; const Title = document.createElement( "h3" ); Title.textContent = "Recipe"; const Pattern = NormalizeAdminTransferMatrix( Mutation.Pattern, null, null, "Any" ); if (Pattern.length === 0) { const Missing = document.createElement( "p" ); Missing.className = "MutationEncyclopediaMissingRecipe"; Missing.textContent = "No recipe data."; Section.append( Title, Missing ); return Section; } const Success = NormalizeAdminTransferMatrix( Mutation.Success, Pattern[0].length, Pattern.length, "Keep" ); const ListOffsets = CreateMutationRecipePreviewOffsets( Pattern, Success ); const Flow = document.createElement( "div" ); Flow.className = "MutationEncyclopediaRecipeFlow"; const Arrow = document.createElement( "span" ); Arrow.className = "MutationEncyclopediaRecipeArrow"; Arrow.textContent = "→"; Arrow.setAttribute( "aria-hidden", "true" ); Flow.append( CreateAdminTransferMutationRecipePanel( "Arrange", Pattern, "Pattern", PlantCatalogue, Mutation.Lists, ListOffsets.Pattern, Mutation.AllowImmature === true ), Arrow, CreateAdminTransferMutationRecipePanel( "Result", Success, "Result", PlantCatalogue, Mutation.Lists, ListOffsets.Result, false ) ); Section.append( Title, Flow ); const Lists = CreateAdminTransferMutationLists( Mutation.Lists, PlantCatalogue ); if (Lists !== null) { Section.appendChild(Lists); } return Section; } function CreateAdminTransferMutationLists( Lists, PlantCatalogue ) { if ( !Array.isArray(Lists) || Lists.length === 0 ) { return null; } const Container = document.createElement("div"); Container.className = "MutationRecipeLists"; const Heading = document.createElement("h4"); Heading.textContent = "Lists"; const OrderedList = document.createElement("ol"); for (const List of Lists) { const ListItem = document.createElement("li"); const Mode = document.createElement("strong"); Mode.textContent = List?.Mode === "Any" ? "Any: " : "Once: "; const Labels = Array.isArray(List?.Items) ? List.Items.map( Item => { if (Item?.Type === "Plant") { return PlantCatalogue?.[ Item.Value ]?.Name ?? Item.Value ?? "Unknown plant"; } if (Item?.Type === "Tag") { return "Tag: " + (Item.Value ?? ""); } if (Item?.Type === "Empty") { return "Empty"; } return "Any"; } ) : []; ListItem.append( Mode, document.createTextNode( Labels.length > 0 ? Labels.join(", ") : "Empty list" ) ); OrderedList.appendChild( ListItem ); } Container.append( Heading, OrderedList ); return Container; } function CreateAdminTransferMutationRecipePanel( Heading, Matrix, Mode, PlantCatalogue, Lists, ListOffsets, CyclePlantStages = false ) { const Panel = document.createElement( "div" ); Panel.className = "MutationEncyclopediaRecipePanel"; const Title = document.createElement( "h4" ); Title.textContent = Heading; const Grid = document.createElement( "div" ); Grid.className = "GuideRecipe MutationRecipeGrid"; Grid.style.setProperty( "--RecipeWidth", String( Matrix[0]?.length ?? 1 ) ); for ( let Y = 0; Y < Matrix.length; Y++ ) { for ( let X = 0; X < Matrix[Y].length; X++ ) { const Value = Matrix[Y][X]; const Cell = CreateAdminTransferMutationCell( Value, Mode, PlantCatalogue, CyclePlantStages ); const ListNumber = GetMutationRecipePreviewListReference( Value ); if (ListNumber !== null) { RegisterMutationRecipePreviewListCell( Cell, Lists?.[ListNumber - 1], ListNumber, ListOffsets?.[Y]?.[X], PlantCatalogue, "MutationRecipeText", Mode === "Pattern" && CyclePlantStages ); } Grid.appendChild(Cell); } } Panel.append( Title, Grid ); return Panel; } function CreateAdminTransferMutationCell( Value, Mode, PlantCatalogue, CyclePlantStages = false ) { if (Mode === "Result") { if ( Value === null || Value === "Keep" ) { return CreateAdminTransferTextCell( "Keep", "MutationRecipeKeep" ); } if (Value === "Empty") { return CreateAdminTransferTextCell( "Empty", "GuideRecipeEmpty" ); } const PlantValue = typeof Value === "string" ? Value : IsAdminPlainObject( Value ) && typeof Value.Plant === "string" ? Value.Plant : null; if (PlantValue === null) { return CreateAdminTransferTextCell( "Keep", "MutationRecipeKeep" ); } if (PlantValue.startsWith("$")) { return CreateAdminTransferTextCell( PlantValue.slice(1), "MutationRecipeCapture" ); } const ListMatch = /^List:([1-9][0-9]*)$/.exec( PlantValue ); if (ListMatch !== null) { return CreateAdminTransferTextCell( "List " + ListMatch[1], "MutationRecipeMatcher" ); } return CreateAdminTransferPlantCell( PlantValue, PlantCatalogue, CyclePlantStages ); } if ( Value === null || Value === "Any" ) { return CreateAdminTransferTextCell( "Any", "MutationRecipeAny" ); } if (Value === "Empty") { return CreateAdminTransferTextCell( "Empty", "GuideRecipeEmpty" ); } if (typeof Value === "string") { const ListMatch = /^List:([1-9][0-9]*)$/.exec( Value ); if (ListMatch !== null) { return CreateAdminTransferTextCell( "List " + ListMatch[1], "MutationRecipeMatcher" ); } return CreateAdminTransferPlantCell( Value, PlantCatalogue, CyclePlantStages ); } if (!IsAdminPlainObject(Value)) { return CreateAdminTransferTextCell( "Any", "MutationRecipeAny" ); } if (typeof Value.Plant === "string") { return CreateAdminTransferPlantCell( Value.Plant, PlantCatalogue, CyclePlantStages ); } const Requirements = []; if ( Array.isArray(Value.Tags) && Value.Tags.length > 0 ) { Requirements.push( Value.Tags.join(" + ") ); } if ( Array.isArray(Value.TagsAny) && Value.TagsAny.length > 0 ) { Requirements.push( "Any: " + Value.TagsAny.join(" / ") ); } if ( Array.isArray(Value.TagsNot) && Value.TagsNot.length > 0 ) { Requirements.push( "Not: " + Value.TagsNot.join(", ") ); } if (Requirements.length === 0) { return CreateAdminTransferTextCell( "Any", "MutationRecipeAny" ); } return CreateAdminTransferTextCell( Requirements.join("\n"), "MutationRecipeMatcher" ); } function CreateAdminTransferPlantCell( PlantKey, PlantCatalogue, CyclePlantStages = false ) { const Plant = PlantCatalogue[PlantKey]; if (Plant === undefined) { return CreateAdminTransferTextCell( PlantKey, "MutationRecipeMatcher" ); } const Element = document.createElement( "div" ); Element.className = "PlantTile GuideRecipeCell GuideRecipePlant"; const MatureImage = GetPlantMatureImageSource( PlantKey ); if (MatureImage !== null) { const Image = document.createElement( "img" ); Image.className = "PlantSprite GuideRecipeImage"; Image.src = MatureImage; Image.alt = ""; Element.appendChild(Image); } const Label = document.createElement( "span" ); Label.className = "GuideRecipeLabel"; Label.textContent = Plant.Name ?? PlantKey; Element.appendChild(Label); if (CyclePlantStages === true) { RegisterMutationRecipePreviewPlantStageCell( Element, Plant ); } return Element; } function CreateAdminTransferTextCell( Text, ClassName ) { const Element = document.createElement( "div" ); Element.className = "PlantTile GuideRecipeCell " + ClassName; const Label = document.createElement( "span" ); Label.className = "MutationRecipeText"; Label.textContent = Text; Element.appendChild(Label); return Element; } function NormalizeAdminTransferMatrix( Matrix, Width = null, Height = null, FillValue = "Any" ) { if (!Array.isArray(Matrix)) { return []; } const TargetHeight = Height ?? Matrix.length; const TargetWidth = Width ?? Math.max( 0, ...Matrix.map( Row => Array.isArray(Row) ? Row.length : 0 ) ); if ( TargetWidth <= 0 || TargetHeight <= 0 ) { return []; } const Result = []; for ( let Y = 0; Y < TargetHeight; Y++ ) { const SourceRow = Array.isArray(Matrix[Y]) ? Matrix[Y] : []; const Row = []; for ( let X = 0; X < TargetWidth; X++ ) { Row.push( X < SourceRow.length ? SourceRow[X] : FillValue ); } Result.push(Row); } return Result; } function CreateAdminTransferMutationRelations( Mutation, PlantCatalogue ) { const Relations = document.createElement( "div" ); Relations.className = "MutationEncyclopediaRelations"; Relations.append( CreateAdminTransferMutationRelationGroup( "Plants used", Mutation.Relations ?.PlantsUsed ?? [], PlantCatalogue ), CreateAdminTransferMutationRelationGroup( "Plants created", Mutation.Relations ?.PlantsCreated ?? [], PlantCatalogue ) ); return Relations; } function CreateAdminTransferMutationRelationGroup( Heading, PlantIds, PlantCatalogue ) { const Group = document.createElement( "section" ); Group.className = "MutationEncyclopediaRelationGroup"; const Title = document.createElement( "h3" ); Title.textContent = Heading; const List = document.createElement( "ul" ); List.className = "MutationEncyclopediaRelationList"; const Ids = [ ...new Set( PlantIds.map( Value => Number(Value) ) ) ].sort( (A, B) => A - B ); if (Ids.length === 0) { const Empty = document.createElement( "li" ); Empty.className = "MutationEncyclopediaRelationEmpty"; Empty.textContent = "None"; List.appendChild(Empty); } else { for (const PlantId of Ids) { const PlantKey = GetAdminTransferPlantKeyById( PlantId, PlantCatalogue ); const Item = document.createElement( "li" ); Item.textContent = PlantKey === null ? "Plant " + PlantId : PlantCatalogue[ PlantKey ]?.Name ?? PlantKey; List.appendChild(Item); } } Group.append( Title, List ); return Group; } function AddAdminTransferCardStatus( Card, StatusText, FileName, ErrorText = null ) { const Header = Card.querySelector( ".PanelHeader" ); const Meta = document.createElement( "div" ); Meta.className = "AdminTransferCardMeta"; const Status = document.createElement( "span" ); Status.className = "AdminTransferStatus"; Status.textContent = StatusText; const File = document.createElement( "span" ); File.className = "AdminTransferFileName"; File.textContent = FileName; Meta.append( Status, File ); Header.appendChild(Meta); if (ErrorText !== null) { const Error = document.createElement( "p" ); Error.className = "AdminTransferInlineError"; Error.textContent = ErrorText; Card.appendChild(Error); } } function FormatAdminTransferChance( Chance ) { const Percent = Math.max( 0, Math.min( Number(Chance ?? 1), 1 ) ) * 100; return Percent.toLocaleString( undefined, { maximumFractionDigits: 2 } ) + "%"; } function FormatAdminTransferDuration( Milliseconds, AllowNone = false ) { let TotalSeconds = Math.max( 0, Math.ceil( Number( Milliseconds ?? 0 ) / 1000 ) ); if ( TotalSeconds === 0 && AllowNone ) { return "None"; } const Hours = Math.floor( TotalSeconds / 3600 ); TotalSeconds %= 3600; const Minutes = Math.floor( TotalSeconds / 60 ); const Seconds = TotalSeconds % 60; const Parts = []; if (Hours > 0) { Parts.push( Hours + "h" ); } if (Minutes > 0) { Parts.push( Minutes + "m" ); } if ( Seconds > 0 || Parts.length === 0 ) { Parts.push( Seconds + "s" ); } return Parts.join(" "); } function CloneAdminTransferValue( Value ) { if (Value === undefined) { return undefined; } return JSON.parse( JSON.stringify(Value) ); } function SetAdminTransferImportMessage( Message ) { const Element = document.getElementById( "AdminMassImportMessage" ); if (Element !== null) { Element.textContent = Message; } } function SetAdminTransferExportMessage( Message ) { const Element = document.getElementById( "AdminMassExportMessage" ); if (Element !== null) { Element.textContent = Message; } } function DownloadAdminTransferZip( FileName, Files ) { const Encoder = new TextEncoder(); const LocalParts = []; const CentralParts = []; let Offset = 0; const Now = new Date(); const DosTime = ( Now.getHours() << 11 ) | ( Now.getMinutes() << 5 ) | Math.floor( Now.getSeconds() / 2 ); const DosDate = ( Math.max( 0, Now.getFullYear() - 1980 ) << 9 ) | ( (Now.getMonth() + 1) << 5 ) | Now.getDate(); for (const File of Files) { const NameBytes = Encoder.encode( File.Name ); const DataBytes = Encoder.encode( File.Text ); const Crc = GetAdminTransferCrc32( DataBytes ); const LocalHeader = new Uint8Array( 30 + NameBytes.length ); const LocalView = new DataView( LocalHeader.buffer ); LocalView.setUint32( 0, 0x04034b50, true ); LocalView.setUint16( 4, 20, true ); LocalView.setUint16( 6, 0x0800, true ); LocalView.setUint16( 8, 0, true ); LocalView.setUint16( 10, DosTime, true ); LocalView.setUint16( 12, DosDate, true ); LocalView.setUint32( 14, Crc, true ); LocalView.setUint32( 18, DataBytes.length, true ); LocalView.setUint32( 22, DataBytes.length, true ); LocalView.setUint16( 26, NameBytes.length, true ); LocalHeader.set( NameBytes, 30 ); LocalParts.push( LocalHeader, DataBytes ); const CentralHeader = new Uint8Array( 46 + NameBytes.length ); const CentralView = new DataView( CentralHeader.buffer ); CentralView.setUint32( 0, 0x02014b50, true ); CentralView.setUint16( 4, 20, true ); CentralView.setUint16( 6, 20, true ); CentralView.setUint16( 8, 0x0800, true ); CentralView.setUint16( 10, 0, true ); CentralView.setUint16( 12, DosTime, true ); CentralView.setUint16( 14, DosDate, true ); CentralView.setUint32( 16, Crc, true ); CentralView.setUint32( 20, DataBytes.length, true ); CentralView.setUint32( 24, DataBytes.length, true ); CentralView.setUint16( 28, NameBytes.length, true ); CentralView.setUint32( 42, Offset, true ); CentralHeader.set( NameBytes, 46 ); CentralParts.push( CentralHeader ); Offset += LocalHeader.length + DataBytes.length; } const CentralOffset = Offset; const CentralSize = CentralParts.reduce( (Total, Part) => Total + Part.length, 0 ); const End = new Uint8Array(22); const EndView = new DataView( End.buffer ); EndView.setUint32( 0, 0x06054b50, true ); EndView.setUint16( 8, Files.length, true ); EndView.setUint16( 10, Files.length, true ); EndView.setUint32( 12, CentralSize, true ); EndView.setUint32( 16, CentralOffset, true ); const BlobData = new Blob( [ ...LocalParts, ...CentralParts, End ], { type: "application/zip" } ); const Url = URL.createObjectURL( BlobData ); const Link = document.createElement( "a" ); Link.href = Url; Link.download = FileName; Link.click(); URL.revokeObjectURL(Url); } function GetAdminTransferCrc32( Data ) { let Crc = 0xffffffff; for (const Byte of Data) { Crc ^= Byte; for ( let Bit = 0; Bit < 8; Bit++ ) { Crc = ( Crc >>> 1 ) ^ ( -(Crc & 1) & 0xedb88320 ); } } return ( Crc ^ 0xffffffff ) >>> 0; } document.addEventListener( "DOMContentLoaded", StartAdminContentTransfer );