diff --git a/Pages/Plants.php b/Pages/Plants.php index 10da3e4..360e319 100644 --- a/Pages/Plants.php +++ b/Pages/Plants.php @@ -72,6 +72,7 @@ $PageSection = "Plants"; + diff --git a/Pages/Profile.php b/Pages/Profile.php index 7356d71..28d0bf4 100644 --- a/Pages/Profile.php +++ b/Pages/Profile.php @@ -99,6 +99,172 @@ $PageTitle = "Profile"; +
+

Gardener profile

+ +
+ + + + +
+ +
+ +

+
+ +

Profile picture

+ +

+ Choose any plant you have discovered. + The selected growth stage is used anywhere + your Gardener profile picture is shown. +

+ +
+
+ + Selected + + + + None + +
+ + + + +
+ +
+ + + +
+ +
+ +

+ Loading discovered plants... +

+ +

+ +

Privacy

+ +

+ Choose which parts of your Gardener profile + other players will be able to use or view. +

+ +
+ + + + + + + +
+ +

+
+
+ + + diff --git a/Scripts/Garden/PlantCatalogue.js b/Scripts/Garden/PlantCatalogue.js new file mode 100644 index 0000000..58f9390 --- /dev/null +++ b/Scripts/Garden/PlantCatalogue.js @@ -0,0 +1,233 @@ +function GetPlantRelatedMutations( + PlantId, + RelationName +) { + return Object.values( + MutationSets + ) + .filter( + Mutation => + Mutation.Relations?.[ + RelationName + ]?.includes( + Number(PlantId) + ) + ) + .sort( + (A, B) => + A.Id - B.Id + ); +} + + +function DoesPlantCatalogueMatchSearch( + Plant, + SearchQuery, + SaveData +) { + if (SearchQuery.length === 0) { + return true; + } + + const SearchParts = [ + Plant.Id, + Plant.Name, + Plant.Description, + ...(Array.isArray(Plant.Tags) + ? Plant.Tags + : []) + ]; + + for ( + const Mutation + of GetPlantRelatedMutations( + Plant.Id, + "PlantsCreated" + ).concat( + GetPlantRelatedMutations( + Plant.Id, + "PlantsUsed" + ) + ) + ) { + if ( + HasDiscoveredMutation( + SaveData, + Mutation.Id + ) + ) { + SearchParts.push( + Mutation.Name + ); + } + } + + return SearchParts + .filter( + Value => + Value !== null && + Value !== undefined + ) + .join(" ") + .toLocaleLowerCase() + .includes( + SearchQuery + ); +} + + +function ComparePlantCataloguePlants( + A, + B, + SortMode, + SaveData +) { + switch (SortMode) { + case "NameAsc": + return String(A.Name ?? "") + .localeCompare( + String(B.Name ?? ""), + undefined, + {sensitivity: "base"} + ) || A.Id - B.Id; + + case "GrowthAsc": + return ComparePlantCatalogueNumbers( + A.GrowthTime, + B.GrowthTime, + 1 + ) || A.Id - B.Id; + + case "GrowthDesc": + return ComparePlantCatalogueNumbers( + A.GrowthTime, + B.GrowthTime, + -1 + ) || A.Id - B.Id; + + case "CostAsc": + return ComparePlantCatalogueNumbers( + GetPlantShopCost( + SaveData, + A.Id + ), + GetPlantShopCost( + SaveData, + B.Id + ), + 1 + ) || A.Id - B.Id; + + case "CostDesc": + return ComparePlantCatalogueNumbers( + GetPlantShopCost( + SaveData, + A.Id + ), + GetPlantShopCost( + SaveData, + B.Id + ), + -1 + ) || A.Id - B.Id; + + case "RewardAsc": + return ComparePlantCatalogueNumbers( + GetPlantHarvestReward( + SaveData, + A.Id + ), + GetPlantHarvestReward( + SaveData, + B.Id + ), + 1 + ) || A.Id - B.Id; + + case "RewardDesc": + return ComparePlantCatalogueNumbers( + GetPlantHarvestReward( + SaveData, + A.Id + ), + GetPlantHarvestReward( + SaveData, + B.Id + ), + -1 + ) || A.Id - B.Id; + + case "DphAsc": + return ComparePlantCatalogueNumbers( + GetPlantDewPerHour( + SaveData, + A.Id + ), + GetPlantDewPerHour( + SaveData, + B.Id + ), + 1 + ) || A.Id - B.Id; + + case "DphDesc": + return ComparePlantCatalogueNumbers( + GetPlantDewPerHour( + SaveData, + A.Id + ), + GetPlantDewPerHour( + SaveData, + B.Id + ), + -1 + ) || A.Id - B.Id; + + case "IdAsc": + default: + return A.Id - B.Id; + } +} + + +function ComparePlantCatalogueNumbers( + A, + B, + Direction +) { + const MissingA = + A === null || + A === undefined || + A === ""; + + const MissingB = + B === null || + B === undefined || + B === ""; + + const NumberA = Number(A); + const NumberB = Number(B); + + const ValidA = + !MissingA && + Number.isFinite(NumberA); + + const ValidB = + !MissingB && + Number.isFinite(NumberB); + + if (!ValidA && !ValidB) { + return 0; + } + + if (!ValidA) { + return 1; + } + + if (!ValidB) { + return -1; + } + + return (NumberA - NumberB) * + Direction; +} diff --git a/Scripts/Garden/PlantEncyclopedia.js b/Scripts/Garden/PlantEncyclopedia.js index e6893d0..ba43baa 100644 --- a/Scripts/Garden/PlantEncyclopedia.js +++ b/Scripts/Garden/PlantEncyclopedia.js @@ -166,54 +166,11 @@ function DoesPlantEncyclopediaMatchSearch( Plant, SearchQuery ) { - if (SearchQuery.length === 0) { - return true; - } - - const SearchParts = [ - Plant.Id, - Plant.Name, - Plant.Description, - ...(Array.isArray(Plant.Tags) - ? Plant.Tags - : []) - ]; - - for ( - const Mutation - of GetPlantRelatedMutations( - Plant.Id, - "PlantsCreated" - ).concat( - GetPlantRelatedMutations( - Plant.Id, - "PlantsUsed" - ) - ) - ) { - if ( - HasDiscoveredMutation( - PlantEncyclopediaSave, - Mutation.Id - ) - ) { - SearchParts.push( - Mutation.Name - ); - } - } - - return SearchParts - .filter( - Value => - Value !== null && - Value !== undefined - ) - .join(" ") - .toLocaleLowerCase() - .includes( - SearchQuery - ); + return DoesPlantCatalogueMatchSearch( + Plant, + SearchQuery, + PlantEncyclopediaSave + ); } @@ -221,154 +178,12 @@ function ComparePlantEncyclopediaPlants( A, B ) { - switch (PlantEncyclopediaSortMode) { - case "NameAsc": - return String(A.Name ?? "") - .localeCompare( - String(B.Name ?? ""), - undefined, - {sensitivity: "base"} - ) || A.Id - B.Id; - - case "GrowthAsc": - return ComparePlantEncyclopediaNumbers( - A.GrowthTime, - B.GrowthTime, - 1 - ) || A.Id - B.Id; - - case "GrowthDesc": - return ComparePlantEncyclopediaNumbers( - A.GrowthTime, - B.GrowthTime, - -1 - ) || A.Id - B.Id; - - case "CostAsc": - return ComparePlantEncyclopediaNumbers( - GetPlantShopCost( - PlantEncyclopediaSave, - A.Id - ), - GetPlantShopCost( - PlantEncyclopediaSave, - B.Id - ), - 1 - ) || A.Id - B.Id; - - case "CostDesc": - return ComparePlantEncyclopediaNumbers( - GetPlantShopCost( - PlantEncyclopediaSave, - A.Id - ), - GetPlantShopCost( - PlantEncyclopediaSave, - B.Id - ), - -1 - ) || A.Id - B.Id; - - case "RewardAsc": - return ComparePlantEncyclopediaNumbers( - GetPlantHarvestReward( - PlantEncyclopediaSave, - A.Id - ), - GetPlantHarvestReward( - PlantEncyclopediaSave, - B.Id - ), - 1 - ) || A.Id - B.Id; - - case "RewardDesc": - return ComparePlantEncyclopediaNumbers( - GetPlantHarvestReward( - PlantEncyclopediaSave, - A.Id - ), - GetPlantHarvestReward( - PlantEncyclopediaSave, - B.Id - ), - -1 - ) || A.Id - B.Id; - - case "DphAsc": - return ComparePlantEncyclopediaNumbers( - GetPlantDewPerHour( - PlantEncyclopediaSave, - A.Id - ), - GetPlantDewPerHour( - PlantEncyclopediaSave, - B.Id - ), - 1 - ) || A.Id - B.Id; - - case "DphDesc": - return ComparePlantEncyclopediaNumbers( - GetPlantDewPerHour( - PlantEncyclopediaSave, - A.Id - ), - GetPlantDewPerHour( - PlantEncyclopediaSave, - B.Id - ), - -1 - ) || A.Id - B.Id; - - case "IdAsc": - default: - return A.Id - B.Id; - } -} - - -function ComparePlantEncyclopediaNumbers( - A, - B, - Direction -) { - const MissingA = - A === null || - A === undefined || - A === ""; - - const MissingB = - B === null || - B === undefined || - B === ""; - - const NumberA = Number(A); - const NumberB = Number(B); - - const ValidA = - !MissingA && - Number.isFinite(NumberA); - - const ValidB = - !MissingB && - Number.isFinite(NumberB); - - if (!ValidA && !ValidB) { - return 0; - } - - if (!ValidA) { - return 1; - } - - if (!ValidB) { - return -1; - } - - return (NumberA - NumberB) * - Direction; + return ComparePlantCataloguePlants( + A, + B, + PlantEncyclopediaSortMode, + PlantEncyclopediaSave + ); } @@ -843,28 +658,6 @@ function CreatePlantMutationRelation( } -function GetPlantRelatedMutations( - PlantId, - RelationName -) { - return Object.values( - MutationSets - ) - .filter( - Mutation => - Mutation.Relations?.[ - RelationName - ]?.includes( - Number(PlantId) - ) - ) - .sort( - (A, B) => - A.Id - B.Id - ); -} - - function GetPlantRecipeProgress( PlantId ) { diff --git a/Scripts/Garden/Profile.js b/Scripts/Garden/Profile.js index 5db6f72..4615f5b 100644 --- a/Scripts/Garden/Profile.js +++ b/Scripts/Garden/Profile.js @@ -1,37 +1,26 @@ let ProfileSave = null; +let ProfileData = null; +let ProfilePictureSearchQuery = ""; +let ProfilePictureSortMode = "IdAsc"; +let ProfilePictureSelectedPlantKey = null; +let ProfilePictureSelectedGrowthStage = null; + +const DefaultGardenerSettings = { + AllowGardenPreviews: true, + AllowComments: true, + AllowDewDonations: true, + ShowStats: true +}; async function StartProfile() { await RenderProfilePlayer(); - try { - await LoadGameContent(); - - ProfileSave = - await LoadGame(); - - RenderProfileStatistics(); - RenderGardenDisplayPreferences(); - BindGardenDisplayPreferences(); - } catch (Error) { - console.error( - "Couldn't load profile statistics:", - Error - ); - - RenderProfileStatisticsError(); - } - const UsernameForm = document.getElementById( "ProfileUsernameForm" ); - UsernameForm.addEventListener( - "submit", - SubmitProfileUsername - ); - const ColourForm = document.getElementById( "ProfileColourForm" @@ -52,14 +41,29 @@ async function StartProfile() { "ResetProfileColourButton" ); - ColourForm.addEventListener( + const DescriptionForm = + document.getElementById( + "ProfileDescriptionForm" + ); + + + UsernameForm?.addEventListener( + "submit", + SubmitProfileUsername + ); + + ColourForm?.addEventListener( "submit", SubmitProfileColour ); - ColourInput.addEventListener( + ColourInput?.addEventListener( "input", () => { + if (ColourPreview === null) { + return; + } + ApplyPlayerColour( ColourPreview, ColourInput.value @@ -67,10 +71,40 @@ async function StartProfile() { } ); - ResetColourButton.addEventListener( + ResetColourButton?.addEventListener( "click", ResetProfileColour ); + + DescriptionForm?.addEventListener( + "submit", + SubmitProfileDescription + ); + + BindProfilePrivacySettings(); + + + try { + await LoadGameContent(); + + ProfileSave = + await LoadGame(); + + RenderProfileStatistics(); + RenderGardenDisplayPreferences(); + BindGardenDisplayPreferences(); + InitialiseProfilePictureChooser(); + } catch (Error) { + console.error( + "Couldn't load profile statistics:", + Error + ); + + RenderProfileStatisticsError(); + SetProfilePictureCatalogueMessage( + "Couldn't load your discovered plants." + ); + } } @@ -540,8 +574,15 @@ async function RenderProfilePlayer() { } + ProfileData = Profile; + RenderProfileDescription(); + RenderProfilePrivacySettings(); + + if ( - Profile.Username !== null + typeof Profile.Username === + "string" && + Profile.Username.length > 0 ) { UsernameInput.value = Profile.Username; @@ -768,6 +809,971 @@ async function ResetProfileColour() { } } +function RenderProfileDescription() { + const Input = + document.getElementById( + "ProfileDescriptionInput" + ); + + if (Input === null) { + return; + } + + Input.value = + typeof ProfileData?.Description === + "string" + ? ProfileData.Description + : ""; +} + + +async function SubmitProfileDescription( + Event +) { + Event.preventDefault(); + + const Input = + document.getElementById( + "ProfileDescriptionInput" + ); + + const Message = + document.getElementById( + "ProfileDescriptionMessage" + ); + + if ( + Input === null || + Message === null + ) { + return; + } + + const Description = + Input.value.trim(); + + if ( + Array.from( + Description + ).length > 500 + ) { + Message.textContent = + "Description must be 500 characters or fewer."; + + return; + } + + Message.textContent = + "Saving..."; + + try { + const Result = + await SetUserConfig({ + Description: Description + }); + + if (!Result.Success) { + Message.textContent = + Result.Error ?? + "Couldn't save description."; + + return; + } + + ProfileData ??= {}; + ProfileData.Description = + Result.Description ?? + Description; + + Input.value = + ProfileData.Description; + + Message.textContent = + "Description saved."; + } catch (Error) { + console.error( + "Couldn't save profile description:", + Error + ); + + Message.textContent = + "Couldn't save description."; + } +} + + +function GetGardenerSettings() { + const StoredSettings = + ProfileData?.Settings !== null && + typeof ProfileData?.Settings === + "object" && + !Array.isArray( + ProfileData.Settings + ) + ? ProfileData.Settings + : {}; + + return { + ...DefaultGardenerSettings, + ...StoredSettings + }; +} + + +function BindProfilePrivacySettings() { + const Settings = [ + [ + "ToggleGardenPreviewsButton", + "AllowGardenPreviews" + ], + [ + "ToggleCommentsButton", + "AllowComments" + ], + [ + "ToggleDewDonationsButton", + "AllowDewDonations" + ], + [ + "ToggleProfileStatsButton", + "ShowStats" + ] + ]; + + for ( + const [ + ButtonId, + SettingName + ] + of Settings + ) { + const Button = + document.getElementById( + ButtonId + ); + + Button?.addEventListener( + "click", + () => { + ToggleProfilePrivacySetting( + SettingName, + Button + ); + } + ); + } +} + + +function RenderProfilePrivacySettings() { + const Settings = + GetGardenerSettings(); + + const Buttons = [ + [ + "ToggleGardenPreviewsButton", + "Garden previews", + "AllowGardenPreviews" + ], + [ + "ToggleCommentsButton", + "Comments", + "AllowComments" + ], + [ + "ToggleDewDonationsButton", + "Dew donations", + "AllowDewDonations" + ], + [ + "ToggleProfileStatsButton", + "Statistics", + "ShowStats" + ] + ]; + + for ( + const [ + ButtonId, + Label, + SettingName + ] + of Buttons + ) { + const Button = + document.getElementById( + ButtonId + ); + + if (Button === null) { + continue; + } + + SetPreferenceButtonState( + Button, + Label, + Settings[SettingName] !== + false + ); + } +} + + +async function ToggleProfilePrivacySetting( + SettingName, + Button +) { + const Message = + document.getElementById( + "ProfilePrivacyMessage" + ); + + const Settings = + GetGardenerSettings(); + + const NewValue = + Settings[SettingName] === + false; + + Button.disabled = true; + + if (Message !== null) { + Message.textContent = + "Saving..."; + } + + try { + const Result = + await SetUserConfig({ + Settings: { + [SettingName]: + NewValue + } + }); + + if (!Result.Success) { + if (Message !== null) { + Message.textContent = + Result.Error ?? + "Couldn't save profile privacy settings."; + } + + return; + } + + ProfileData ??= {}; + ProfileData.Settings = + Result.Settings ?? { + ...Settings, + [SettingName]: NewValue + }; + + RenderProfilePrivacySettings(); + + if (Message !== null) { + Message.textContent = + "Privacy setting saved."; + } + } catch (Error) { + console.error( + "Couldn't save profile privacy setting:", + Error + ); + + if (Message !== null) { + Message.textContent = + "Couldn't save profile privacy settings."; + } + } finally { + Button.disabled = false; + } +} + + +function InitialiseProfilePictureChooser() { + const SearchInput = + document.getElementById( + "ProfilePictureSearchInput" + ); + + const SortSelect = + document.getElementById( + "ProfilePictureSortSelect" + ); + + const GrowthStageSelect = + document.getElementById( + "ProfilePictureGrowthStageSelect" + ); + + const SaveButton = + document.getElementById( + "SaveProfilePictureButton" + ); + + + if (SearchInput !== null) { + SearchInput.value = + ProfilePictureSearchQuery; + + SearchInput.addEventListener( + "input", + () => { + ProfilePictureSearchQuery = + SearchInput.value; + + RenderProfilePictureGrid(); + } + ); + } + + if (SortSelect !== null) { + SortSelect.value = + ProfilePictureSortMode; + + SortSelect.addEventListener( + "change", + () => { + ProfilePictureSortMode = + SortSelect.value; + + RenderProfilePictureGrid(); + } + ); + } + + GrowthStageSelect?.addEventListener( + "change", + () => { + const GrowthStage = + Number( + GrowthStageSelect.value + ); + + if ( + Number.isInteger( + GrowthStage + ) && + GrowthStage > 0 + ) { + ProfilePictureSelectedGrowthStage = + GrowthStage; + + RenderProfilePictureGrid(); + } + } + ); + + SaveButton?.addEventListener( + "click", + SaveProfilePicture + ); + + + const SavedProfilePicture = + ProfileData?.ProfilePicture; + + if ( + SavedProfilePicture !== null && + typeof SavedProfilePicture === + "object" && + !Array.isArray( + SavedProfilePicture + ) && + typeof SavedProfilePicture.PlantKey === + "string" && + IsProfilePicturePlantAvailable( + SavedProfilePicture.PlantKey + ) + ) { + ProfilePictureSelectedPlantKey = + SavedProfilePicture.PlantKey; + + ProfilePictureSelectedGrowthStage = + Number( + SavedProfilePicture.GrowthStage + ); + } else { + const FirstEntry = + GetProfilePicturePlantEntries() + .sort( + (A, B) => + A.Plant.Id - + B.Plant.Id + )[0] ?? null; + + ProfilePictureSelectedPlantKey = + FirstEntry?.PlantKey ?? + null; + + ProfilePictureSelectedGrowthStage = + null; + } + + RenderProfilePictureChooser(); +} + + +function GetProfilePicturePlantEntries() { + if (ProfileSave === null) { + return []; + } + + return Object.entries( + Plants + ) + .filter( + ([, Plant]) => + HasDiscoveredPlant( + ProfileSave, + Plant.Id + ) + ) + .map( + ([PlantKey, Plant]) => ({ + PlantKey: PlantKey, + Plant: Plant + }) + ); +} + + +function IsProfilePicturePlantAvailable( + PlantKey +) { + const Plant = + Plants[PlantKey]; + + return ( + Plant !== undefined && + ProfileSave !== null && + HasDiscoveredPlant( + ProfileSave, + Plant.Id + ) + ); +} + + +function GetProfilePictureAvailableStages( + Plant +) { + return GetPlantImageSources( + Plant + ) + .map( + (Source, Index) => ({ + Stage: Index + 1, + Source: Source + }) + ) + .filter( + Item => + typeof Item.Source === + "string" && + Item.Source.length > 0 + ); +} + + +function GetProfilePictureMatureStage( + Plant +) { + const Stages = + GetProfilePictureAvailableStages( + Plant + ); + + return Stages.length === 0 + ? null + : Stages[ + Stages.length - 1 + ].Stage; +} + + +function GetProfilePictureStageSource( + Plant, + GrowthStage +) { + const Stages = + GetProfilePictureAvailableStages( + Plant + ); + + const RequestedStage = + Stages.find( + Item => + Item.Stage === + GrowthStage + ); + + if (RequestedStage !== undefined) { + return RequestedStage.Source; + } + + return Stages.length === 0 + ? null + : Stages[ + Stages.length - 1 + ].Source; +} + + +function RenderProfilePictureChooser() { + RenderProfilePictureSelection(); + RenderProfilePictureGrid(); +} + + +function RenderProfilePictureSelection() { + const Name = + document.getElementById( + "ProfilePictureSelectedName" + ); + + const GrowthStageSelect = + document.getElementById( + "ProfilePictureGrowthStageSelect" + ); + + const SaveButton = + document.getElementById( + "SaveProfilePictureButton" + ); + + if ( + Name === null || + GrowthStageSelect === null || + SaveButton === null + ) { + return; + } + + const Plant = + ProfilePictureSelectedPlantKey === + null + ? null + : Plants[ + ProfilePictureSelectedPlantKey + ] ?? null; + + GrowthStageSelect.replaceChildren(); + + if (Plant === null) { + Name.textContent = + "None"; + + const Option = + document.createElement( + "option" + ); + + Option.value = ""; + Option.textContent = "None"; + + GrowthStageSelect.appendChild( + Option + ); + + GrowthStageSelect.disabled = true; + SaveButton.disabled = true; + + return; + } + + Name.textContent = + Plant.Name; + + const Stages = + GetProfilePictureAvailableStages( + Plant + ); + + if (Stages.length === 0) { + const Option = + document.createElement( + "option" + ); + + Option.value = ""; + Option.textContent = + "No image"; + + GrowthStageSelect.appendChild( + Option + ); + + GrowthStageSelect.disabled = true; + SaveButton.disabled = true; + + return; + } + + const ValidSelectedStage = + Stages.some( + Item => + Item.Stage === + ProfilePictureSelectedGrowthStage + ); + + if (!ValidSelectedStage) { + ProfilePictureSelectedGrowthStage = + Stages[ + Stages.length - 1 + ].Stage; + } + + for ( + let Index = 0; + Index < Stages.length; + Index++ + ) { + const Stage = + Stages[Index]; + + const Option = + document.createElement( + "option" + ); + + Option.value = + String( + Stage.Stage + ); + + Option.textContent = + Index === Stages.length - 1 + ? "Mature" + : "Stage " + + Stage.Stage; + + GrowthStageSelect.appendChild( + Option + ); + } + + GrowthStageSelect.value = + String( + ProfilePictureSelectedGrowthStage + ); + + GrowthStageSelect.disabled = false; + SaveButton.disabled = false; +} + + +function RenderProfilePictureGrid() { + const Grid = + document.getElementById( + "ProfilePictureGrid" + ); + + if (Grid === null) { + return; + } + + Grid.replaceChildren(); + + const Entries = + GetProfilePicturePlantEntries(); + + const SearchQuery = + ProfilePictureSearchQuery + .trim() + .toLocaleLowerCase(); + + const VisibleEntries = + Entries + .filter( + Entry => + DoesPlantCatalogueMatchSearch( + Entry.Plant, + SearchQuery, + ProfileSave + ) + ) + .sort( + (A, B) => + ComparePlantCataloguePlants( + A.Plant, + B.Plant, + ProfilePictureSortMode, + ProfileSave + ) + ); + + if (Entries.length === 0) { + SetProfilePictureCatalogueMessage( + "No plants discovered yet." + ); + + return; + } + + if (VisibleEntries.length === 0) { + SetProfilePictureCatalogueMessage( + "No discovered plants match your search." + ); + + return; + } + + for ( + const Entry + of VisibleEntries + ) { + Grid.appendChild( + CreateProfilePictureOption( + Entry + ) + ); + } + + if (SearchQuery.length > 0) { + SetProfilePictureCatalogueMessage( + "Showing " + + VisibleEntries.length + .toLocaleString() + + " of " + + Entries.length + .toLocaleString() + + " discovered plants." + ); + + return; + } + + SetProfilePictureCatalogueMessage( + Entries.length === 1 + ? "1 plant available." + : Entries.length + .toLocaleString() + + " plants available." + ); +} + + +function CreateProfilePictureOption( + Entry +) { + const Button = + document.createElement( + "button" + ); + + Button.type = "button"; + Button.className = + "ProfilePictureOption"; + + Button.setAttribute( + "aria-pressed", + Entry.PlantKey === + ProfilePictureSelectedPlantKey + ? "true" + : "false" + ); + + + const Name = + document.createElement( + "span" + ); + + Name.className = + "ProfilePictureOptionName"; + + Name.textContent = + Entry.Plant.Name; + + + const Visual = + document.createElement( + "span" + ); + + Visual.className = + "PlantTile ProfilePictureOptionVisual"; + + + const ImageSource = + GetProfilePictureStageSource( + Entry.Plant, + ProfilePictureSelectedGrowthStage + ); + + if (ImageSource === null) { + const Missing = + document.createElement( + "span" + ); + + Missing.className = + "ProfilePictureOptionMissing"; + + Missing.textContent = + "No image"; + + Visual.appendChild( + Missing + ); + } else { + const Image = + document.createElement( + "img" + ); + + Image.className = + "PlantSprite"; + + Image.src = + ImageSource; + + Image.alt = + Entry.Plant.Name; + + Visual.appendChild( + Image + ); + } + + + Button.append( + Name, + Visual + ); + + Button.addEventListener( + "click", + () => { + ProfilePictureSelectedPlantKey = + Entry.PlantKey; + + const AvailableStages = + GetProfilePictureAvailableStages( + Entry.Plant + ); + + if ( + !AvailableStages.some( + Item => + Item.Stage === + ProfilePictureSelectedGrowthStage + ) + ) { + ProfilePictureSelectedGrowthStage = + GetProfilePictureMatureStage( + Entry.Plant + ); + } + + RenderProfilePictureChooser(); + } + ); + + + return Button; +} + + +function SetProfilePictureCatalogueMessage( + Text +) { + const Message = + document.getElementById( + "ProfilePictureCatalogueMessage" + ); + + if (Message !== null) { + Message.textContent = + Text; + } +} + + +async function SaveProfilePicture() { + const Message = + document.getElementById( + "ProfilePictureMessage" + ); + + const SaveButton = + document.getElementById( + "SaveProfilePictureButton" + ); + + if ( + ProfilePictureSelectedPlantKey === + null || + !Number.isInteger( + ProfilePictureSelectedGrowthStage + ) || + ProfilePictureSelectedGrowthStage < 1 + ) { + if (Message !== null) { + Message.textContent = + "Choose a plant with an image first."; + } + + return; + } + + if (SaveButton !== null) { + SaveButton.disabled = true; + } + + if (Message !== null) { + Message.textContent = + "Saving..."; + } + + try { + const Result = + await SetUserConfig({ + ProfilePicture: { + PlantKey: + ProfilePictureSelectedPlantKey, + GrowthStage: + ProfilePictureSelectedGrowthStage + } + }); + + if (!Result.Success) { + if (Message !== null) { + Message.textContent = + Result.Error ?? + "Couldn't save profile picture."; + } + + return; + } + + ProfileData ??= {}; + ProfileData.ProfilePicture = + Result.ProfilePicture; + + if (Message !== null) { + Message.textContent = + "Profile picture saved."; + } + } catch (Error) { + console.error( + "Couldn't save profile picture:", + Error + ); + + if (Message !== null) { + Message.textContent = + "Couldn't save profile picture."; + } + } finally { + if (SaveButton !== null) { + SaveButton.disabled = false; + } + } +} + + function RenderProfileStatistics() { const CurrentDew = ProfileSave diff --git a/Scripts/Garden/Users.js b/Scripts/Garden/Users.js index cecbbf9..a4572d0 100644 --- a/Scripts/Garden/Users.js +++ b/Scripts/Garden/Users.js @@ -233,6 +233,29 @@ async function SetUsername( return await Response.json(); } +async function SetUserConfig( + Changes +) { + const Response = await fetch( + ApiUrl + "/SetUserConfig.php", + { + method: "POST", + + headers: { + "Content-Type": "application/json" + }, + + body: JSON.stringify({ + SaveKey: GetSaveKey(), + ...Changes + }) + } + ); + + return await Response.json(); +} + + async function SetColour( Colour ) { diff --git a/Styles/Style.css b/Styles/Style.css index fd1826c..cb5c5cc 100644 --- a/Styles/Style.css +++ b/Styles/Style.css @@ -1046,7 +1046,7 @@ input, button, textarea, select { color: var(--Blue); } -.ProfileSection input, .ProfileSection textarea, .ProfileSection button { +.ProfileSection input, .ProfileSection textarea, .ProfileSection select, .ProfileSection button { font-size: 1rem; color: var(--Text); background-color: var(--Surface0); @@ -1055,7 +1055,7 @@ input, button, textarea, select { appearance: none; } -.ProfileSection input, .ProfileSection textarea { +.ProfileSection input, .ProfileSection textarea, .ProfileSection select { padding: 0.6rem; outline: none; } @@ -1064,6 +1064,10 @@ input, button, textarea, select { min-width: 12rem; } +.ProfileSection select { + appearance: auto; +} + .ProfileSection textarea { width: 100%; max-width: 24rem; @@ -1074,12 +1078,12 @@ input, button, textarea, select { color: var(--Overlay1); } -.ProfileSection input:hover, .ProfileSection textarea:hover { +.ProfileSection input:hover, .ProfileSection textarea:hover, .ProfileSection select:hover { background-color: var(--Surface1); border-color: var(--Overlay2); } -.ProfileSection input:focus, .ProfileSection textarea:focus { +.ProfileSection input:focus, .ProfileSection textarea:focus, .ProfileSection select:focus { background-color: var(--Surface1); border-color: var(--Sky); } @@ -1260,7 +1264,7 @@ input, button, textarea, select { align-items: stretch; } - .ProfileSection input, .ProfileSection textarea, .ProfileSection button { + .ProfileSection input, .ProfileSection textarea, .ProfileSection select, .ProfileSection button { width: 100%; } @@ -1524,6 +1528,87 @@ input, button, textarea, select { color: var(--Text); } +.ProfilePictureSelectionRow { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(10rem, auto) auto; + gap: 0.75rem; + margin-bottom: 1rem; + align-items: end; +} + +.ProfilePictureSelectionLabel { + display: block; + margin-bottom: 0.35rem; + color: var(--Subtext1); +} + +.ProfilePictureGrowthStageLabel { + margin-bottom: 0 !important; +} + +.ProfilePictureGrowthStageLabel select { + display: block; + width: 100%; + min-width: 10rem; + margin-top: 0.35rem; + cursor: pointer; +} + +.ProfilePictureGrid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 0.75rem; + max-height: 31rem; + padding: 0.25rem; + overflow-y: auto; +} + +.ProfileSection .ProfilePictureOption { + display: flex; + min-width: 0; + padding: 0.5rem; + flex-direction: column; + gap: 0.4rem; + align-items: stretch; +} + +.ProfileSection .ProfilePictureOption[aria-pressed="true"] { + color: var(--Yellow); + border-color: var(--Yellow); +} + +.ProfilePictureOptionName { + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ProfilePictureOptionVisual { + display: flex; + width: 100%; + aspect-ratio: 1; + align-items: center; + justify-content: center; +} + +.ProfilePictureOptionMissing { + padding: 0.35rem; + color: var(--Overlay1); + font-size: 0.75rem; + text-align: center; +} + +@media (max-width: 700px) { + .ProfilePictureSelectionRow { + grid-template-columns: 1fr 1fr; + } + + .ProfilePictureSelectionRow > :first-child { + grid-column: 1 / -1; + } +} + /* * Garden guide */ .GuideSection {