Compare commits
5 commits
4025dcd968
...
c2de6f7167
Author | SHA1 | Date | |
---|---|---|---|
c2de6f7167 | |||
3ebbfc4967 | |||
741d52175b | |||
8e5939e408 | |||
33bcabab83 |
12 changed files with 173 additions and 46 deletions
|
@ -11,8 +11,9 @@ class AltStylesController < ApplicationController
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { render }
|
format.html { render }
|
||||||
format.json {
|
format.json {
|
||||||
render json: @alt_styles.as_json(
|
render json: @alt_styles.includes(swf_assets: [:zone]).as_json(
|
||||||
methods: [:adjective_name, :thumbnail_url],
|
include: {swf_assets: {include: [:zone], methods: [:image_url]}},
|
||||||
|
methods: [:series_name, :adjective_name, :thumbnail_url],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -215,12 +215,6 @@ function OutfitControls({
|
||||||
* We try to center the species/color picker, but the left spacer will
|
* We try to center the species/color picker, but the left spacer will
|
||||||
* shrink more than the pose picker container if we run out of space!
|
* shrink more than the pose picker container if we run out of space!
|
||||||
*/}
|
*/}
|
||||||
<Flex
|
|
||||||
flex="1 1 0"
|
|
||||||
paddingRight="3"
|
|
||||||
align="center"
|
|
||||||
justify="flex-end"
|
|
||||||
/>
|
|
||||||
<Box flex="0 0 auto">
|
<Box flex="0 0 auto">
|
||||||
<DarkMode>
|
<DarkMode>
|
||||||
<SpeciesColorPicker
|
<SpeciesColorPicker
|
||||||
|
@ -234,11 +228,12 @@ function OutfitControls({
|
||||||
/>
|
/>
|
||||||
</DarkMode>
|
</DarkMode>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex flex="1 1 0" align="center" pl="2">
|
<Flex flex="0 0 auto" align="center" pl="2">
|
||||||
<PosePicker
|
<PosePicker
|
||||||
speciesId={outfitState.speciesId}
|
speciesId={outfitState.speciesId}
|
||||||
colorId={outfitState.colorId}
|
colorId={outfitState.colorId}
|
||||||
pose={outfitState.pose}
|
pose={outfitState.pose}
|
||||||
|
altStyleId={outfitState.altStyleId}
|
||||||
appearanceId={outfitState.appearanceId}
|
appearanceId={outfitState.appearanceId}
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
onLockFocus={onLockFocus}
|
onLockFocus={onLockFocus}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
useToast,
|
useToast,
|
||||||
useToken,
|
useToken,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import { ChevronDownIcon } from "@chakra-ui/icons";
|
||||||
import { loadable } from "../util";
|
import { loadable } from "../util";
|
||||||
|
|
||||||
import { petAppearanceFragment } from "../components/useOutfitAppearance";
|
import { petAppearanceFragment } from "../components/useOutfitAppearance";
|
||||||
|
@ -65,6 +66,7 @@ function PosePicker({
|
||||||
speciesId,
|
speciesId,
|
||||||
colorId,
|
colorId,
|
||||||
pose,
|
pose,
|
||||||
|
altStyleId,
|
||||||
appearanceId,
|
appearanceId,
|
||||||
dispatchToOutfit,
|
dispatchToOutfit,
|
||||||
onLockFocus,
|
onLockFocus,
|
||||||
|
@ -86,8 +88,24 @@ function PosePicker({
|
||||||
const poseInfos = posesQuery.poseInfos;
|
const poseInfos = posesQuery.poseInfos;
|
||||||
const altStyles = altStylesQuery.data ?? [];
|
const altStyles = altStylesQuery.data ?? [];
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [tabIndex, setTabIndex] = React.useState(0);
|
||||||
|
|
||||||
|
const altStyle = altStyles.find((s) => s.id === altStyleId);
|
||||||
|
|
||||||
const placement = useBreakpointValue({ base: "bottom-end", md: "top-end" });
|
const placement = useBreakpointValue({ base: "bottom-end", md: "top-end" });
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// While the popover is open, don't change which tab is open.
|
||||||
|
if (isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, set the tab to Styles if we're wearing an Alt Style, or
|
||||||
|
// Expressions if we're not.
|
||||||
|
setTabIndex(altStyle != null ? 1 : 0);
|
||||||
|
}, [altStyle, isOpen]);
|
||||||
|
|
||||||
// Resize the Popover when we toggle support mode, because it probably will
|
// Resize the Popover when we toggle support mode, because it probably will
|
||||||
// affect the content size.
|
// affect the content size.
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
|
@ -144,10 +162,6 @@ function PosePicker({
|
||||||
dispatchToOutfit,
|
dispatchToOutfit,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a low-stakes enough control, where enough pairs don't have data
|
// This is a low-stakes enough control, where enough pairs don't have data
|
||||||
// anyway, that I think I want to just not draw attention to failures.
|
// anyway, that I think I want to just not draw attention to failures.
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -158,24 +172,40 @@ function PosePicker({
|
||||||
(p) => p.isAvailable && STANDARD_POSES.includes(p.pose),
|
(p) => p.isAvailable && STANDARD_POSES.includes(p.pose),
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const onChange = (e) => {
|
const onChangePose = (e) => {
|
||||||
dispatchToOutfit({ type: "setPose", pose: e.target.value });
|
dispatchToOutfit({ type: "setPose", pose: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeStyle = (altStyleId) => {
|
||||||
|
dispatchToOutfit({ type: "setStyle", altStyleId });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
placement={placement}
|
placement={placement}
|
||||||
returnFocusOnClose
|
returnFocusOnClose
|
||||||
onOpen={onLockFocus}
|
onOpen={() => {
|
||||||
onClose={onUnlockFocus}
|
setIsOpen(true);
|
||||||
|
onLockFocus();
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
onUnlockFocus();
|
||||||
|
}}
|
||||||
initialFocusRef={initialFocusRef}
|
initialFocusRef={initialFocusRef}
|
||||||
isLazy
|
isLazy
|
||||||
lazyBehavior="keepMounted"
|
lazyBehavior="keepMounted"
|
||||||
>
|
>
|
||||||
{({ isOpen }) => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<PosePickerButton pose={pose} isOpen={isOpen} {...props} />
|
<PosePickerButton
|
||||||
|
pose={pose}
|
||||||
|
altStyle={altStyle}
|
||||||
|
isOpen={isOpen}
|
||||||
|
loading={loading}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal>
|
<Portal>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
|
@ -186,6 +216,8 @@ function PosePicker({
|
||||||
flexDirection={{ base: "column", md: "column-reverse" }}
|
flexDirection={{ base: "column", md: "column-reverse" }}
|
||||||
paddingY="2"
|
paddingY="2"
|
||||||
gap="2"
|
gap="2"
|
||||||
|
index={tabIndex}
|
||||||
|
onChange={setTabIndex}
|
||||||
// HACK: To only apply `initialFocusRef` to the selected input
|
// HACK: To only apply `initialFocusRef` to the selected input
|
||||||
// in the *active* tab, we just use `isLazy` to only *render*
|
// in the *active* tab, we just use `isLazy` to only *render*
|
||||||
// the active tab. We could also watch the tab state and set
|
// the active tab. We could also watch the tab state and set
|
||||||
|
@ -213,7 +245,7 @@ function PosePicker({
|
||||||
<>
|
<>
|
||||||
<PosePickerTable
|
<PosePickerTable
|
||||||
poseInfos={poseInfos}
|
poseInfos={poseInfos}
|
||||||
onChange={onChange}
|
onChange={onChangePose}
|
||||||
initialFocusRef={initialFocusRef}
|
initialFocusRef={initialFocusRef}
|
||||||
/>
|
/>
|
||||||
{numStandardPoses == 0 && (
|
{numStandardPoses == 0 && (
|
||||||
|
@ -232,7 +264,9 @@ function PosePicker({
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel paddingX="4" paddingY="0">
|
<TabPanel paddingX="4" paddingY="0">
|
||||||
<StyleSelect
|
<StyleSelect
|
||||||
|
selectedStyleId={altStyleId}
|
||||||
altStyles={altStyles}
|
altStyles={altStyles}
|
||||||
|
onChange={onChangeStyle}
|
||||||
initialFocusRef={initialFocusRef}
|
initialFocusRef={initialFocusRef}
|
||||||
/>
|
/>
|
||||||
<StyleExplanation />
|
<StyleExplanation />
|
||||||
|
@ -248,9 +282,12 @@ function PosePicker({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PosePickerButton({ pose, isOpen, ...props }, ref) {
|
function PosePickerButton({ pose, altStyle, isOpen, loading, ...props }, ref) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const icon = altStyle != null ? twemojiSunglasses : getIcon(pose);
|
||||||
|
const label = altStyle != null ? altStyle.seriesName : getLabel(pose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClassNames>
|
<ClassNames>
|
||||||
{({ css, cx }) => (
|
{({ css, cx }) => (
|
||||||
|
@ -265,16 +302,28 @@ function PosePickerButton({ pose, isOpen, ...props }, ref) {
|
||||||
outline="initial"
|
outline="initial"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
fontWeight="normal"
|
fontWeight="normal"
|
||||||
|
minWidth="12ch"
|
||||||
|
disabled={loading}
|
||||||
className={cx(
|
className={cx(
|
||||||
css`
|
css`
|
||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
transition: border-color 0.2s !important;
|
color: ${theme.colors.gray["300"]};
|
||||||
padding-inline: 0.75em;
|
cursor: ${loading ? "wait" : "pointer"} !important;
|
||||||
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
border-color 0.2s !important;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.is-open {
|
&.is-open {
|
||||||
border-color: ${theme.colors.gray["50"]} !important;
|
border-color: ${theme.colors.gray["50"]} !important;
|
||||||
|
color: ${theme.colors.gray["50"]};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: ${theme.colors.gray["50"]} !important;
|
||||||
|
box-shadow: ${theme.shadows.outline};
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-open {
|
&.is-open {
|
||||||
|
@ -286,11 +335,11 @@ function PosePickerButton({ pose, isOpen, ...props }, ref) {
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<EmojiImage src={getIcon(pose)} alt="" />
|
<EmojiImage src={icon} alt="Style" />
|
||||||
<SupportOnly>
|
|
||||||
<Box width=".5em" />
|
<Box width=".5em" />
|
||||||
{getLabel(pose)}
|
{label}
|
||||||
</SupportOnly>
|
<Box width=".5em" />
|
||||||
|
<ChevronDownIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ClassNames>
|
</ClassNames>
|
||||||
|
@ -572,9 +621,12 @@ function PosePickerEmptyExplanation() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StyleSelect({ altStyles, initialFocusRef }) {
|
function StyleSelect({
|
||||||
const [selectedStyleId, setSelectedStyleId] = React.useState(null);
|
selectedStyleId,
|
||||||
|
altStyles,
|
||||||
|
onChange,
|
||||||
|
initialFocusRef,
|
||||||
|
}) {
|
||||||
const defaultStyle = { id: null, adjectiveName: "Default" };
|
const defaultStyle = { id: null, adjectiveName: "Default" };
|
||||||
|
|
||||||
const styles = [defaultStyle, ...altStyles];
|
const styles = [defaultStyle, ...altStyles];
|
||||||
|
@ -593,7 +645,7 @@ function StyleSelect({ altStyles, initialFocusRef }) {
|
||||||
key={altStyle.id}
|
key={altStyle.id}
|
||||||
altStyle={altStyle}
|
altStyle={altStyle}
|
||||||
checked={selectedStyleId === altStyle.id}
|
checked={selectedStyleId === altStyle.id}
|
||||||
onChange={setSelectedStyleId}
|
onChange={onChange}
|
||||||
inputRef={selectedStyleId === altStyle.id ? initialFocusRef : null}
|
inputRef={selectedStyleId === altStyle.id ? initialFocusRef : null}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -24,6 +24,7 @@ function WardrobePreviewAndControls({
|
||||||
speciesId: outfitState.speciesId,
|
speciesId: outfitState.speciesId,
|
||||||
colorId: outfitState.colorId,
|
colorId: outfitState.colorId,
|
||||||
pose: outfitState.pose,
|
pose: outfitState.pose,
|
||||||
|
altStyleId: outfitState.altStyleId,
|
||||||
appearanceId: outfitState.appearanceId,
|
appearanceId: outfitState.appearanceId,
|
||||||
wornItemIds: outfitState.wornItemIds,
|
wornItemIds: outfitState.wornItemIds,
|
||||||
onChangeHasAnimations: setHasAnimations,
|
onChangeHasAnimations: setHasAnimations,
|
||||||
|
|
|
@ -413,11 +413,12 @@ function ItemSupportPetCompatibilityRuleFields({
|
||||||
*/
|
*/
|
||||||
function ItemSupportAppearanceLayers({ item }) {
|
function ItemSupportAppearanceLayers({ item }) {
|
||||||
const outfitState = React.useContext(OutfitStateContext);
|
const outfitState = React.useContext(OutfitStateContext);
|
||||||
const { speciesId, colorId, pose, appearanceId } = outfitState;
|
const { speciesId, colorId, pose, altStyleId, appearanceId } = outfitState;
|
||||||
const { error, visibleLayers } = useOutfitAppearance({
|
const { error, visibleLayers } = useOutfitAppearance({
|
||||||
speciesId,
|
speciesId,
|
||||||
colorId,
|
colorId,
|
||||||
pose,
|
pose,
|
||||||
|
altStyleId,
|
||||||
appearanceId,
|
appearanceId,
|
||||||
wornItemIds: [item.id],
|
wornItemIds: [item.id],
|
||||||
});
|
});
|
||||||
|
|
|
@ -113,7 +113,8 @@ function useOutfitState() {
|
||||||
// IDs. It's more convenient to manage them as a Set in state, but most
|
// IDs. It's more convenient to manage them as a Set in state, but most
|
||||||
// callers will find it more convenient to access them as arrays! e.g. for
|
// callers will find it more convenient to access them as arrays! e.g. for
|
||||||
// `.map()`.
|
// `.map()`.
|
||||||
const { id, name, speciesId, colorId, pose, appearanceId } = outfitState;
|
const { id, name, speciesId, colorId, pose, altStyleId, appearanceId } =
|
||||||
|
outfitState;
|
||||||
const wornItemIds = Array.from(outfitState.wornItemIds);
|
const wornItemIds = Array.from(outfitState.wornItemIds);
|
||||||
const closetedItemIds = Array.from(outfitState.closetedItemIds);
|
const closetedItemIds = Array.from(outfitState.closetedItemIds);
|
||||||
const allItemIds = [...wornItemIds, ...closetedItemIds];
|
const allItemIds = [...wornItemIds, ...closetedItemIds];
|
||||||
|
@ -236,6 +237,7 @@ function useOutfitState() {
|
||||||
speciesId,
|
speciesId,
|
||||||
colorId,
|
colorId,
|
||||||
pose,
|
pose,
|
||||||
|
altStyleId,
|
||||||
appearanceId,
|
appearanceId,
|
||||||
url,
|
url,
|
||||||
|
|
||||||
|
@ -351,6 +353,10 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => {
|
||||||
// particular about which version of the pose to show if more than one.
|
// particular about which version of the pose to show if more than one.
|
||||||
state.appearanceId = action.appearanceId || null;
|
state.appearanceId = action.appearanceId || null;
|
||||||
});
|
});
|
||||||
|
case "setStyle":
|
||||||
|
return produce(baseState, (state) => {
|
||||||
|
state.altStyleId = action.altStyleId;
|
||||||
|
});
|
||||||
case "resetToSavedOutfitData":
|
case "resetToSavedOutfitData":
|
||||||
return getOutfitStateFromOutfitData(action.savedOutfitData);
|
return getOutfitStateFromOutfitData(action.savedOutfitData);
|
||||||
default:
|
default:
|
||||||
|
@ -417,6 +423,7 @@ function readOutfitStateFromSearchParams(pathname, searchParams) {
|
||||||
speciesId: searchParams.get("species") || "1",
|
speciesId: searchParams.get("species") || "1",
|
||||||
colorId: searchParams.get("color") || "8",
|
colorId: searchParams.get("color") || "8",
|
||||||
pose: searchParams.get("pose") || "HAPPY_FEM",
|
pose: searchParams.get("pose") || "HAPPY_FEM",
|
||||||
|
altStyleId: searchParams.get("style") || null,
|
||||||
appearanceId: searchParams.get("state") || null,
|
appearanceId: searchParams.get("state") || null,
|
||||||
wornItemIds: new Set(searchParams.getAll("objects[]")),
|
wornItemIds: new Set(searchParams.getAll("objects[]")),
|
||||||
closetedItemIds: new Set(searchParams.getAll("closet[]")),
|
closetedItemIds: new Set(searchParams.getAll("closet[]")),
|
||||||
|
@ -640,6 +647,7 @@ function buildOutfitQueryString(outfitState) {
|
||||||
speciesId,
|
speciesId,
|
||||||
colorId,
|
colorId,
|
||||||
pose,
|
pose,
|
||||||
|
altStyleId,
|
||||||
appearanceId,
|
appearanceId,
|
||||||
wornItemIds,
|
wornItemIds,
|
||||||
closetedItemIds,
|
closetedItemIds,
|
||||||
|
@ -657,6 +665,9 @@ function buildOutfitQueryString(outfitState) {
|
||||||
for (const itemId of closetedItemIds) {
|
for (const itemId of closetedItemIds) {
|
||||||
params.append("closet[]", itemId);
|
params.append("closet[]", itemId);
|
||||||
}
|
}
|
||||||
|
if (altStyleId != null) {
|
||||||
|
params.append("style", altStyleId);
|
||||||
|
}
|
||||||
if (appearanceId != null) {
|
if (appearanceId != null) {
|
||||||
// `state` is an old name for compatibility with old-style DTI URLs. It
|
// `state` is an old name for compatibility with old-style DTI URLs. It
|
||||||
// refers to "PetState", the database table name for pet appearances.
|
// refers to "PetState", the database table name for pet appearances.
|
||||||
|
|
|
@ -52,6 +52,7 @@ export function useOutfitPreview({
|
||||||
speciesId,
|
speciesId,
|
||||||
colorId,
|
colorId,
|
||||||
pose,
|
pose,
|
||||||
|
altStyleId,
|
||||||
wornItemIds,
|
wornItemIds,
|
||||||
appearanceId = null,
|
appearanceId = null,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
@ -68,6 +69,7 @@ export function useOutfitPreview({
|
||||||
speciesId,
|
speciesId,
|
||||||
colorId,
|
colorId,
|
||||||
pose,
|
pose,
|
||||||
|
altStyleId,
|
||||||
appearanceId,
|
appearanceId,
|
||||||
wornItemIds,
|
wornItemIds,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
|
|
||||||
import getVisibleLayers, {
|
import getVisibleLayers, {
|
||||||
itemAppearanceFragmentForGetVisibleLayers,
|
itemAppearanceFragmentForGetVisibleLayers,
|
||||||
petAppearanceFragmentForGetVisibleLayers,
|
petAppearanceFragmentForGetVisibleLayers,
|
||||||
} from "../components/getVisibleLayers";
|
} from "./getVisibleLayers";
|
||||||
|
import { useAltStyle } from "../loaders/alt-styles";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useOutfitAppearance downloads the outfit's appearance data, and returns
|
* useOutfitAppearance downloads the outfit's appearance data, and returns
|
||||||
* visibleLayers for rendering.
|
* visibleLayers for rendering.
|
||||||
*/
|
*/
|
||||||
export default function useOutfitAppearance(outfitState) {
|
export default function useOutfitAppearance(outfitState) {
|
||||||
const { wornItemIds, speciesId, colorId, pose, appearanceId } = outfitState;
|
const { wornItemIds, speciesId, colorId, pose, altStyleId, appearanceId } =
|
||||||
|
outfitState;
|
||||||
|
|
||||||
// We split this query out from the other one, so that we can HTTP cache it.
|
// We split this query out from the other one, so that we can HTTP cache it.
|
||||||
//
|
//
|
||||||
|
@ -102,7 +105,13 @@ export default function useOutfitAppearance(outfitState) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const petAppearance = data1?.petAppearance;
|
const {
|
||||||
|
isLoading: loading3,
|
||||||
|
error: error3,
|
||||||
|
data: altStyle,
|
||||||
|
} = useAltStyle(altStyleId, speciesId);
|
||||||
|
|
||||||
|
const petAppearance = altStyle?.appearance ?? data1?.petAppearance;
|
||||||
const items = data2?.items;
|
const items = data2?.items;
|
||||||
const itemAppearances = React.useMemo(
|
const itemAppearances = React.useMemo(
|
||||||
() => (items || []).map((i) => i.appearance),
|
() => (items || []).map((i) => i.appearance),
|
||||||
|
@ -116,8 +125,8 @@ export default function useOutfitAppearance(outfitState) {
|
||||||
const bodyId = petAppearance?.bodyId;
|
const bodyId = petAppearance?.bodyId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading: loading1 || loading2,
|
loading: loading1 || loading2 || loading3,
|
||||||
error: error1 || error2,
|
error: error1 || error2 || error3,
|
||||||
petAppearance,
|
petAppearance,
|
||||||
items: items || [],
|
items: items || [],
|
||||||
itemAppearances,
|
itemAppearances,
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#3B88C3" d="M14.57 27.673c2.814-1.692 6.635-3.807 9.899-7.071 7.03-7.029 12.729-16.97 11.314-18.385C34.369.803 24.428 6.502 17.398 13.531c-3.265 3.265-5.379 7.085-7.071 9.899l4.243 4.243z"/><path fill="#C1694F" d="M.428 34.744s7.071 1.414 12.021-3.536c2.121-2.121 2.121-4.949 2.121-4.949l-2.829-2.829s-3.535.708-4.95 2.122c-1.414 1.414-2.518 4.232-2.888 5.598-.676 2.502-3.475 3.594-3.475 3.594z"/><path fill="#CCD6DD" d="M17.882 25.328l-5.168-5.168c-.391-.391-.958-.326-1.27.145l-1.123 1.705c-.311.471-.271 1.142.087 1.501l4.122 4.123c.358.358 1.03.397 1.501.087l1.705-1.124c.472-.311.536-.878.146-1.269z"/><path fill="#A0041E" d="M11.229 32.26c-1.191.769-1.826.128-1.609-.609.221-.751-.12-1.648-1.237-1.414-1.117.233-1.856-.354-1.503-1.767.348-1.393-1.085-1.863-1.754-.435-.582 1.16-1.017 2.359-1.222 3.115-.677 2.503-3.476 3.595-3.476 3.595s5.988 1.184 10.801-2.485z"/></svg>
|
|
Before Width: | Height: | Size: 950 B |
|
@ -8,6 +8,17 @@ export function useAltStylesForSpecies(speciesId, options = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: This is actually just a wrapper for `useAltStylesForSpecies`, to share
|
||||||
|
// the same cache key!
|
||||||
|
export function useAltStyle(id, speciesId, options = {}) {
|
||||||
|
const query = useAltStylesForSpecies(speciesId, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
data: query.data?.find((s) => s.id === id) ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAltStylesForSpecies(speciesId) {
|
async function loadAltStylesForSpecies(speciesId) {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/species/${encodeURIComponent(speciesId)}/alt-styles.json`,
|
`/species/${encodeURIComponent(speciesId)}/alt-styles.json`,
|
||||||
|
@ -28,11 +39,44 @@ function normalizeAltStyles(altStylesData) {
|
||||||
|
|
||||||
function normalizeAltStyle(altStyleData) {
|
function normalizeAltStyle(altStyleData) {
|
||||||
return {
|
return {
|
||||||
id: altStyleData.id,
|
id: String(altStyleData.id),
|
||||||
speciesId: altStyleData.species_id,
|
speciesId: String(altStyleData.species_id),
|
||||||
colorId: altStyleData.color_id,
|
colorId: String(altStyleData.color_id),
|
||||||
bodyId: altStyleData.body_id,
|
bodyId: String(altStyleData.body_id),
|
||||||
|
seriesName: altStyleData.series_name,
|
||||||
adjectiveName: altStyleData.adjective_name,
|
adjectiveName: altStyleData.adjective_name,
|
||||||
thumbnailUrl: altStyleData.thumbnail_url,
|
thumbnailUrl: altStyleData.thumbnail_url,
|
||||||
|
|
||||||
|
// This matches the PetAppearanceForOutfitPreview GQL fragment!
|
||||||
|
appearance: {
|
||||||
|
bodyId: String(altStyleData.body_id),
|
||||||
|
pose: "UNKNOWN",
|
||||||
|
isGlitched: false,
|
||||||
|
species: { id: String(altStyleData.species_id) },
|
||||||
|
color: { id: String(altStyleData.species_id) },
|
||||||
|
layers: altStyleData.swf_assets.map(normalizeSwfAssetToLayer),
|
||||||
|
restrictedZones: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSwfAssetToLayer(swfAssetData) {
|
||||||
|
return {
|
||||||
|
id: String(swfAssetData.id),
|
||||||
|
zone: {
|
||||||
|
id: String(swfAssetData.zone.id),
|
||||||
|
depth: swfAssetData.zone.depth,
|
||||||
|
label: swfAssetData.zone.label,
|
||||||
|
},
|
||||||
|
bodyId: swfAssetData.body_id,
|
||||||
|
knownGlitches: [], // TODO
|
||||||
|
|
||||||
|
// HACK: We're just simplifying this adapter, but it would be better to
|
||||||
|
// actually check what file formats the manifest says!
|
||||||
|
// TODO: For example, these do generally have SVGs, we could use them!
|
||||||
|
svgUrl: null,
|
||||||
|
canvasMovieLibraryUrl: null,
|
||||||
|
imageUrl: swfAssetData.image_url,
|
||||||
|
swfUrl: swfAssetData.url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,25 @@ class AltStyle < ApplicationRecord
|
||||||
has_many :swf_assets, through: :parent_swf_asset_relationships
|
has_many :swf_assets, through: :parent_swf_asset_relationships
|
||||||
has_many :contributions, as: :contributed, inverse_of: :contributed
|
has_many :contributions, as: :contributed, inverse_of: :contributed
|
||||||
|
|
||||||
|
SERIES_ID_RANGES = {
|
||||||
|
nostalgic: (87249..87503)
|
||||||
|
}
|
||||||
|
|
||||||
def name
|
def name
|
||||||
I18n.translate('pet_types.human_name', color_human_name: color.human_name,
|
I18n.translate('pet_types.human_name', color_human_name: color.human_name,
|
||||||
species_human_name: species.human_name)
|
species_human_name: species.human_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def series_name
|
||||||
|
if SERIES_ID_RANGES[:nostalgic].include?(id)
|
||||||
|
"Nostalgic"
|
||||||
|
else
|
||||||
|
"???"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def adjective_name
|
def adjective_name
|
||||||
"Nostalgic #{color.human_name}"
|
"#{series_name} #{color.human_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail_url
|
def thumbnail_url
|
||||||
|
|
|
@ -46,7 +46,7 @@ class SwfAsset < ApplicationRecord
|
||||||
size_key = size.join('x')
|
size_key = size.join('x')
|
||||||
|
|
||||||
image_dir = "#{self['type']}/#{partition_path}#{self.remote_id}"
|
image_dir = "#{self['type']}/#{partition_path}#{self.remote_id}"
|
||||||
"//#{host}/#{image_dir}/#{size_key}.png?#{image_version}"
|
"https://#{host}/#{image_dir}/#{size_key}.png?#{image_version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def images
|
def images
|
||||||
|
|
Loading…
Reference in a new issue