forked from OpenNeo/impress-2020
I think it's confusing that the poses in the dropdown start with the emotion word, but are grouped by the gender presentation word! It's also different than the precedence order! I've reordered them.
479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
import React from "react";
|
|
import gql from "graphql-tag";
|
|
import { useMutation, useQuery } from "@apollo/client";
|
|
import { Box, IconButton, Select, Spinner, Switch } from "@chakra-ui/react";
|
|
import {
|
|
ArrowBackIcon,
|
|
ArrowForwardIcon,
|
|
CheckCircleIcon,
|
|
} from "@chakra-ui/icons";
|
|
|
|
import HangerSpinner from "../../components/HangerSpinner";
|
|
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
|
|
import useSupport from "./useSupport";
|
|
|
|
function PosePickerSupport({
|
|
speciesId,
|
|
colorId,
|
|
pose,
|
|
appearanceId,
|
|
initialFocusRef,
|
|
dispatchToOutfit,
|
|
}) {
|
|
const { loading, error, data } = useQuery(
|
|
gql`
|
|
query PosePickerSupport($speciesId: ID!, $colorId: ID!) {
|
|
petAppearances(speciesId: $speciesId, colorId: $colorId) {
|
|
id
|
|
pose
|
|
isGlitched
|
|
layers {
|
|
id
|
|
zone {
|
|
id
|
|
label @client
|
|
}
|
|
}
|
|
}
|
|
|
|
...CanonicalPetAppearances
|
|
}
|
|
${canonicalPetAppearancesFragment}
|
|
`,
|
|
{ variables: { speciesId, colorId } }
|
|
);
|
|
|
|
// Resize the Popover when we toggle loading state, because it probably will
|
|
// affect the content size. appearanceId might also affect content size, if
|
|
// it occupies different zones.
|
|
//
|
|
// NOTE: This also triggers an additional necessary resize when the component
|
|
// first mounts, because PosePicker lazy-loads it, so it actually
|
|
// mounting affects size too.
|
|
React.useLayoutEffect(() => {
|
|
// HACK: To trigger a Popover resize, we simulate a window resize event,
|
|
// because Popover listens for window resizes to reposition itself.
|
|
// I've also filed an issue requesting an official API!
|
|
// https://github.com/chakra-ui/chakra-ui/issues/1853
|
|
window.dispatchEvent(new Event("resize"));
|
|
}, [loading, appearanceId]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box display="flex" justifyContent="center">
|
|
<HangerSpinner size="sm" />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Box color="red.400" marginTop="8">
|
|
{error.message}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const canonicalAppearanceIdsByPose = {
|
|
HAPPY_MASC: data.happyMasc?.id,
|
|
SAD_MASC: data.sadMasc?.id,
|
|
SICK_MASC: data.sickMasc?.id,
|
|
HAPPY_FEM: data.happyFem?.id,
|
|
SAD_FEM: data.sadFem?.id,
|
|
SICK_FEM: data.sickFem?.id,
|
|
UNCONVERTED: data.unconverted?.id,
|
|
UNKNOWN: data.unknown?.id,
|
|
};
|
|
const canonicalAppearanceIds = Object.values(
|
|
canonicalAppearanceIdsByPose
|
|
).filter((id) => id);
|
|
|
|
if (!appearanceId) {
|
|
appearanceId = canonicalAppearanceIdsByPose[pose];
|
|
}
|
|
|
|
const currentPetAppearance = data.petAppearances.find(
|
|
(pa) => pa.id === appearanceId
|
|
);
|
|
if (!currentPetAppearance) {
|
|
return (
|
|
<Box color="red.400" marginTop="8">
|
|
Pet appearance with ID {JSON.stringify(appearanceId)} not found
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<PosePickerSupportNavigator
|
|
petAppearances={data.petAppearances}
|
|
currentPetAppearance={currentPetAppearance}
|
|
canonicalAppearanceIds={canonicalAppearanceIds}
|
|
dropdownRef={initialFocusRef}
|
|
dispatchToOutfit={dispatchToOutfit}
|
|
/>
|
|
<Metadata
|
|
fontSize="sm"
|
|
// Build a new copy of this tree when the appearance changes, to reset
|
|
// things like element focus and mutation state!
|
|
key={currentPetAppearance.id}
|
|
>
|
|
<MetadataLabel>DTI ID:</MetadataLabel>
|
|
<MetadataValue>{appearanceId}</MetadataValue>
|
|
<MetadataLabel>Pose:</MetadataLabel>
|
|
<MetadataValue>
|
|
<PosePickerSupportPoseFields
|
|
petAppearance={currentPetAppearance}
|
|
speciesId={speciesId}
|
|
colorId={colorId}
|
|
/>
|
|
</MetadataValue>
|
|
<MetadataLabel>Zones:</MetadataLabel>
|
|
<MetadataValue>
|
|
{currentPetAppearance.layers
|
|
.map((l) => l.zone)
|
|
.map((z) => `${z.label} (${z.id})`)
|
|
.sort()
|
|
.join(", ")}
|
|
</MetadataValue>
|
|
</Metadata>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function PosePickerSupportNavigator({
|
|
petAppearances,
|
|
currentPetAppearance,
|
|
canonicalAppearanceIds,
|
|
dropdownRef,
|
|
dispatchToOutfit,
|
|
}) {
|
|
const currentIndex = petAppearances.indexOf(currentPetAppearance);
|
|
const prevPetAppearance = petAppearances[currentIndex - 1];
|
|
const nextPetAppearance = petAppearances[currentIndex + 1];
|
|
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
justifyContent="flex-end"
|
|
marginBottom="4"
|
|
// Space for the position-absolute PosePicker mode switcher
|
|
paddingLeft="12"
|
|
>
|
|
<IconButton
|
|
aria-label="Go to previous appearance"
|
|
icon={<ArrowBackIcon />}
|
|
size="sm"
|
|
marginRight="2"
|
|
isDisabled={prevPetAppearance == null}
|
|
onClick={() =>
|
|
dispatchToOutfit({
|
|
type: "setPose",
|
|
pose: prevPetAppearance.pose,
|
|
appearanceId: prevPetAppearance.id,
|
|
})
|
|
}
|
|
/>
|
|
<Select
|
|
size="sm"
|
|
width="auto"
|
|
value={currentPetAppearance.id}
|
|
ref={dropdownRef}
|
|
onChange={(e) => {
|
|
const id = e.target.value;
|
|
const petAppearance = petAppearances.find((pa) => pa.id === id);
|
|
dispatchToOutfit({
|
|
type: "setPose",
|
|
pose: petAppearance.pose,
|
|
appearanceId: petAppearance.id,
|
|
});
|
|
}}
|
|
>
|
|
{petAppearances.map((pa) => (
|
|
<option key={pa.id} value={pa.id}>
|
|
{POSE_NAMES[pa.pose]}{" "}
|
|
{canonicalAppearanceIds.includes(pa.id) && "⭐️"}
|
|
{pa.isGlitched && "👾"} ({pa.id})
|
|
</option>
|
|
))}
|
|
</Select>
|
|
<IconButton
|
|
aria-label="Go to next appearance"
|
|
icon={<ArrowForwardIcon />}
|
|
size="sm"
|
|
marginLeft="2"
|
|
isDisabled={nextPetAppearance == null}
|
|
onClick={() =>
|
|
dispatchToOutfit({
|
|
type: "setPose",
|
|
pose: nextPetAppearance.pose,
|
|
appearanceId: nextPetAppearance.id,
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function PosePickerSupportPoseFields({ petAppearance, speciesId, colorId }) {
|
|
const { supportSecret } = useSupport();
|
|
|
|
const [mutatePose, poseMutation] = useMutation(
|
|
gql`
|
|
mutation PosePickerSupportSetPetAppearancePose(
|
|
$appearanceId: ID!
|
|
$pose: Pose!
|
|
$supportSecret: String!
|
|
) {
|
|
setPetAppearancePose(
|
|
appearanceId: $appearanceId
|
|
pose: $pose
|
|
supportSecret: $supportSecret
|
|
) {
|
|
id
|
|
pose
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
refetchQueries: [
|
|
{
|
|
query: gql`
|
|
query PosePickerSupportRefetchCanonicalAppearances(
|
|
$speciesId: ID!
|
|
$colorId: ID!
|
|
) {
|
|
...CanonicalPetAppearances
|
|
}
|
|
${canonicalPetAppearancesFragment}
|
|
`,
|
|
variables: { speciesId, colorId },
|
|
},
|
|
],
|
|
}
|
|
);
|
|
|
|
const [mutateIsGlitched, isGlitchedMutation] = useMutation(
|
|
gql`
|
|
mutation PosePickerSupportSetPetAppearanceIsGlitched(
|
|
$appearanceId: ID!
|
|
$isGlitched: Boolean!
|
|
$supportSecret: String!
|
|
) {
|
|
setPetAppearanceIsGlitched(
|
|
appearanceId: $appearanceId
|
|
isGlitched: $isGlitched
|
|
supportSecret: $supportSecret
|
|
) {
|
|
id
|
|
isGlitched
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
refetchQueries: [
|
|
{
|
|
query: gql`
|
|
query PosePickerSupportRefetchCanonicalAppearances(
|
|
$speciesId: ID!
|
|
$colorId: ID!
|
|
) {
|
|
...CanonicalPetAppearances
|
|
}
|
|
${canonicalPetAppearancesFragment}
|
|
`,
|
|
variables: { speciesId, colorId },
|
|
},
|
|
],
|
|
}
|
|
);
|
|
|
|
return (
|
|
<Box>
|
|
<Box display="flex" flexDirection="row" alignItems="center">
|
|
<Select
|
|
size="sm"
|
|
value={petAppearance.pose}
|
|
flex="0 1 200px"
|
|
icon={
|
|
poseMutation.loading ? (
|
|
<Spinner />
|
|
) : poseMutation.data ? (
|
|
<CheckCircleIcon />
|
|
) : undefined
|
|
}
|
|
onChange={(e) => {
|
|
const pose = e.target.value;
|
|
mutatePose({
|
|
variables: {
|
|
appearanceId: petAppearance.id,
|
|
pose,
|
|
supportSecret,
|
|
},
|
|
optimisticResponse: {
|
|
__typename: "Mutation",
|
|
setPetAppearancePose: {
|
|
__typename: "PetAppearance",
|
|
id: petAppearance.id,
|
|
pose,
|
|
},
|
|
},
|
|
}).catch((e) => {
|
|
/* Discard errors here; we'll show them in the UI! */
|
|
});
|
|
}}
|
|
isInvalid={poseMutation.error != null}
|
|
>
|
|
{Object.entries(POSE_NAMES).map(([pose, name]) => (
|
|
<option key={pose} value={pose}>
|
|
{name}
|
|
</option>
|
|
))}
|
|
</Select>
|
|
<Select
|
|
size="sm"
|
|
marginLeft="2"
|
|
flex="0 1 150px"
|
|
value={petAppearance.isGlitched}
|
|
icon={
|
|
isGlitchedMutation.loading ? (
|
|
<Spinner />
|
|
) : isGlitchedMutation.data ? (
|
|
<CheckCircleIcon />
|
|
) : undefined
|
|
}
|
|
onChange={(e) => {
|
|
const isGlitched = e.target.value === "true";
|
|
mutateIsGlitched({
|
|
variables: {
|
|
appearanceId: petAppearance.id,
|
|
isGlitched,
|
|
supportSecret,
|
|
},
|
|
optimisticResponse: {
|
|
__typename: "Mutation",
|
|
setPetAppearanceIsGlitched: {
|
|
__typename: "PetAppearance",
|
|
id: petAppearance.id,
|
|
isGlitched,
|
|
},
|
|
},
|
|
}).catch((e) => {
|
|
/* Discard errors here; we'll show them in the UI! */
|
|
});
|
|
}}
|
|
isInvalid={isGlitchedMutation.error != null}
|
|
>
|
|
<option value="false">Valid</option>
|
|
<option value="true">Glitched</option>
|
|
</Select>
|
|
</Box>
|
|
{poseMutation.error && (
|
|
<Box color="red.400">{poseMutation.error.message}</Box>
|
|
)}
|
|
{isGlitchedMutation.error && (
|
|
<Box color="red.400">{isGlitchedMutation.error.message}</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export function PosePickerSupportSwitch({ isChecked, onChange }) {
|
|
return (
|
|
<Box as="label" display="flex" flexDirection="row" alignItems="center">
|
|
<Box fontSize="sm">
|
|
<span role="img" aria-label="Support">
|
|
💖
|
|
</span>
|
|
</Box>
|
|
<Switch
|
|
colorScheme="pink"
|
|
marginLeft="1"
|
|
size="sm"
|
|
isChecked={isChecked}
|
|
onChange={onChange}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const POSE_NAMES = {
|
|
HAPPY_MASC: "Happy Masc",
|
|
HAPPY_FEM: "Happy Fem",
|
|
SAD_MASC: "Sad Masc",
|
|
SAD_FEM: "Sad Fem",
|
|
SICK_MASC: "Sick Masc",
|
|
SICK_FEM: "Sick Fem",
|
|
UNCONVERTED: "Unconverted",
|
|
UNKNOWN: "Unknown",
|
|
};
|
|
|
|
const canonicalPetAppearancesFragment = gql`
|
|
fragment CanonicalPetAppearances on Query {
|
|
happyMasc: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: HAPPY_MASC
|
|
) {
|
|
id
|
|
}
|
|
|
|
sadMasc: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: SAD_MASC
|
|
) {
|
|
id
|
|
}
|
|
|
|
sickMasc: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: SICK_MASC
|
|
) {
|
|
id
|
|
}
|
|
|
|
happyFem: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: HAPPY_FEM
|
|
) {
|
|
id
|
|
}
|
|
|
|
sadFem: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: SAD_FEM
|
|
) {
|
|
id
|
|
}
|
|
|
|
sickFem: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: SICK_FEM
|
|
) {
|
|
id
|
|
}
|
|
|
|
unconverted: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: UNCONVERTED
|
|
) {
|
|
id
|
|
}
|
|
|
|
unknown: petAppearance(
|
|
speciesId: $speciesId
|
|
colorId: $colorId
|
|
pose: UNKNOWN
|
|
) {
|
|
id
|
|
}
|
|
}
|
|
`;
|
|
|
|
export default PosePickerSupport;
|