Add WIP styles tab to the pose picker

It shows the styles! You can select between them, but it currently does
nothing, womp womp!
This commit is contained in:
Emi Matchu 2024-01-29 03:20:48 -08:00
parent e2ab8bbc9c
commit 514c99fb42
4 changed files with 180 additions and 3 deletions

View file

@ -10,7 +10,11 @@ class AltStylesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html { render } format.html { render }
format.json { render json: @alt_styles } format.json {
render json: @alt_styles.as_json(
methods: [:adjective_name, :thumbnail_url],
)
}
end end
end end
end end

View file

@ -20,6 +20,7 @@ import {
useColorModeValue, useColorModeValue,
useTheme, useTheme,
useToast, useToast,
useToken,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { loadable } from "../util"; import { loadable } from "../util";
@ -27,6 +28,7 @@ import { petAppearanceFragment } from "../components/useOutfitAppearance";
import getVisibleLayers from "../components/getVisibleLayers"; import getVisibleLayers from "../components/getVisibleLayers";
import { OutfitLayers } from "../components/OutfitPreview"; import { OutfitLayers } from "../components/OutfitPreview";
import SupportOnly from "./support/SupportOnly"; import SupportOnly from "./support/SupportOnly";
import { useAltStylesForSpecies } from "../loaders/alt-styles";
import useSupport from "./support/useSupport"; import useSupport from "./support/useSupport";
import { useLocalStorage } from "../util"; import { useLocalStorage } from "../util";
@ -69,7 +71,8 @@ function PosePicker({
...props ...props
}) { }) {
const initialFocusRef = React.useRef(); const initialFocusRef = React.useRef();
const { loading, error, poseInfos } = usePoses(speciesId, colorId, pose); const posesQuery = usePoses(speciesId, colorId, pose);
const altStylesQuery = useAltStylesForSpecies(speciesId);
const [isInSupportMode, setIsInSupportMode] = useLocalStorage( const [isInSupportMode, setIsInSupportMode] = useLocalStorage(
"DTIPosePickerIsInSupportMode", "DTIPosePickerIsInSupportMode",
false, false,
@ -77,6 +80,11 @@ function PosePicker({
const { isSupportUser } = useSupport(); const { isSupportUser } = useSupport();
const toast = useToast(); const toast = useToast();
const loading = posesQuery.loading || altStylesQuery.loading;
const error = posesQuery.error ?? altStylesQuery.error;
const poseInfos = posesQuery.poseInfos;
const altStyles = altStylesQuery.data ?? [];
// 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(() => {
@ -201,7 +209,10 @@ function PosePicker({
</Box> </Box>
</SupportOnly> </SupportOnly>
</TabPanel> </TabPanel>
<TabPanel>WIP: Styles go here!</TabPanel> <TabPanel>
<StyleSelect altStyles={altStyles} />
<StyleExplanation />
</TabPanel>
</TabPanels> </TabPanels>
<TabList paddingX="2" paddingY="1"> <TabList paddingX="2" paddingY="1">
<Tab width="50%">Expressions</Tab> <Tab width="50%">Expressions</Tab>
@ -539,6 +550,126 @@ function PosePickerEmptyExplanation() {
); );
} }
function StyleSelect({ altStyles }) {
const [selectedStyleId, setSelectedStyleId] = React.useState(null);
const defaultStyle = { id: null, adjectiveName: "Default" };
return (
<Flex
as="form"
direction="column"
gap="2"
maxHeight="40vh"
padding="4px"
overflow="auto"
>
<StyleOption
altStyle={defaultStyle}
checked={selectedStyleId == null}
onChange={setSelectedStyleId}
/>
{altStyles.map((altStyle) => (
<StyleOption
key={altStyle.id}
altStyle={altStyle}
checked={selectedStyleId === altStyle.id}
onChange={setSelectedStyleId}
/>
))}
</Flex>
);
}
function StyleOption({ altStyle, checked, onChange }) {
const theme = useTheme();
const selectedBorderColor = useColorModeValue(
theme.colors.green["600"],
theme.colors.green["300"],
);
const outlineShadow = useToken("shadows", "outline");
return (
<ClassNames>
{({ css, cx }) => (
<Box
as="label"
cursor="pointer"
onClick={(e) => {
// HACK: We need the timeout to beat the popover's focus stealing!
const input = e.currentTarget.querySelector("input");
setTimeout(() => input.focus(), 0);
}}
>
<VisuallyHidden
as="input"
type="radio"
name="style"
value={altStyle.id}
checked={checked}
onChange={(e) => onChange(altStyle.id)}
/>
<Flex
alignItems="center"
gap="2"
borderRadius="md"
// HACK: Don't let the thumbnail image overlap the border
paddingLeft="2px"
paddingY="2px"
className={cx(css`
border: 2px solid transparent;
opacity: 0.8;
transition: all 0.2s;
input:focus + & {
box-shadow: ${outlineShadow};
}
input:checked + & {
border-color: ${selectedBorderColor};
opacity: 1;
font-weight: bold;
}
`)}
>
{altStyle.thumbnailUrl ? (
<Box
as="img"
src={altStyle.thumbnailUrl}
alt="Item thumbnail"
width="40px"
height="40px"
loading="lazy"
/>
) : (
<Box width="40px" height="40px" />
)}
<Box width="2" />
{altStyle.adjectiveName}
</Flex>
</Box>
)}
</ClassNames>
);
}
function StyleExplanation() {
return (
<Box
fontSize="xs"
fontStyle="italic"
textAlign="center"
opacity="0.7"
marginTop="2"
>
"Alt Styles" are special NC items that override the pet's usual appearance
via the "Styling Studio". The pet's color doesn't have to match.
<br />
WIP: The styles can't actually be applied yet!
</Box>
);
}
function EmojiImage({ src, alt, boxSize = 16 }) { function EmojiImage({ src, alt, boxSize = 16 }) {
return <img src={src} alt={alt} width={boxSize} height={boxSize} />; return <img src={src} alt={alt} width={boxSize} height={boxSize} />;
} }

View file

@ -0,0 +1,38 @@
import { useQuery } from "@tanstack/react-query";
export function useAltStylesForSpecies(speciesId, options = {}) {
return useQuery({
...options,
queryKey: ["altStylesForSpecies", String(speciesId)],
queryFn: () => loadAltStylesForSpecies(speciesId),
});
}
async function loadAltStylesForSpecies(speciesId) {
const res = await fetch(
`/species/${encodeURIComponent(speciesId)}/alt-styles.json`,
);
if (!res.ok) {
throw new Error(
`loading alt styles failed: ${res.status} ${res.statusText}`,
);
}
return res.json().then(normalizeAltStyles);
}
function normalizeAltStyles(altStylesData) {
return altStylesData.map(normalizeAltStyle);
}
function normalizeAltStyle(altStyleData) {
return {
id: altStyleData.id,
speciesId: altStyleData.species_id,
colorId: altStyleData.color_id,
bodyId: altStyleData.body_id,
adjectiveName: altStyleData.adjective_name,
thumbnailUrl: altStyleData.thumbnail_url,
};
}

View file

@ -11,6 +11,10 @@ class AltStyle < ApplicationRecord
species_human_name: species.human_name) species_human_name: species.human_name)
end end
def adjective_name
"Nostalgic #{color.human_name}"
end
def thumbnail_url def thumbnail_url
# HACK: Just assume this is a Nostalgic Alt Style, and that the thumbnail # HACK: Just assume this is a Nostalgic Alt Style, and that the thumbnail
# is named reliably! # is named reliably!