Add "Use DTI's image archive" option

just a lil thing for people to turn on if it gets truly miserable again!!
This commit is contained in:
Emi Matchu 2022-10-13 16:44:20 -07:00
parent 8dee9ddbed
commit 4619e86ae0
7 changed files with 117 additions and 36 deletions

View file

@ -43,6 +43,7 @@ import { loadImage, useLocalStorage } from "../util";
import useCurrentUser from "../components/useCurrentUser"; import useCurrentUser from "../components/useCurrentUser";
import useOutfitAppearance from "../components/useOutfitAppearance"; import useOutfitAppearance from "../components/useOutfitAppearance";
import OutfitKnownGlitchesBadge from "./OutfitKnownGlitchesBadge"; import OutfitKnownGlitchesBadge from "./OutfitKnownGlitchesBadge";
import usePreferArchive from "../components/usePreferArchive";
/** /**
* OutfitControls is the set of controls layered over the outfit preview, to * OutfitControls is the set of controls layered over the outfit preview, to
@ -500,28 +501,56 @@ function SettingsButton({ onLockFocus, onUnlockFocus }) {
function HiResModeSetting() { function HiResModeSetting() {
const [hiResMode, setHiResMode] = useLocalStorage("DTIHiResMode", false); const [hiResMode, setHiResMode] = useLocalStorage("DTIHiResMode", false);
const [preferArchive, setPreferArchive] = usePreferArchive();
return ( return (
<FormControl> <Box>
<Flex> <FormControl>
<Box> <Flex>
<FormLabel htmlFor="hi-res-mode-setting" fontSize="sm" margin="0"> <Box>
Hi-res mode (SVG) <FormLabel htmlFor="hi-res-mode-setting" fontSize="sm" margin="0">
</FormLabel> Hi-res mode (SVG)
<FormHelperText marginTop="0" fontSize="xs"> </FormLabel>
Crisper at higher resolutions, but not always accurate <FormHelperText marginTop="0" fontSize="xs">
</FormHelperText> Crisper at higher resolutions, but not always accurate
</Box> </FormHelperText>
<Box width="2" /> </Box>
<Switch <Box width="2" />
id="hi-res-mode-setting" <Switch
size="sm" id="hi-res-mode-setting"
marginTop="0.1rem" size="sm"
isChecked={hiResMode} marginTop="0.1rem"
onChange={(e) => setHiResMode(e.target.checked)} isChecked={hiResMode}
/> onChange={(e) => setHiResMode(e.target.checked)}
</Flex> />
</FormControl> </Flex>
</FormControl>
<Box height="2" />
<FormControl>
<Flex>
<Box>
<FormLabel
htmlFor="prefer-archive-setting"
fontSize="sm"
margin="0"
>
Use DTI's image archive
</FormLabel>
<FormHelperText marginTop="0" fontSize="xs">
Turn this on when images.neopets.com is slow!
</FormHelperText>
</Box>
<Box width="2" />
<Switch
id="prefer-archive-setting"
size="sm"
marginTop="0.1rem"
isChecked={preferArchive ?? false}
onChange={(e) => setPreferArchive(e.target.checked)}
/>
</Flex>
</FormControl>
</Box>
); );
} }
@ -585,6 +614,7 @@ function ControlButton({ icon, "aria-label": ariaLabel, ...props }) {
*/ */
function useDownloadableImage(visibleLayers) { function useDownloadableImage(visibleLayers) {
const [hiResMode] = useLocalStorage("DTIHiResMode", false); const [hiResMode] = useLocalStorage("DTIHiResMode", false);
const [preferArchive] = usePreferArchive();
const [downloadImageUrl, setDownloadImageUrl] = React.useState(null); const [downloadImageUrl, setDownloadImageUrl] = React.useState(null);
const [preparedForLayerIds, setPreparedForLayerIds] = React.useState([]); const [preparedForLayerIds, setPreparedForLayerIds] = React.useState([]);
@ -611,6 +641,7 @@ function useDownloadableImage(visibleLayers) {
const imagePromises = visibleLayers.map((layer) => const imagePromises = visibleLayers.map((layer) =>
loadImage(getBestImageUrlForLayer(layer, { hiResMode }), { loadImage(getBestImageUrlForLayer(layer, { hiResMode }), {
crossOrigin: "anonymous", crossOrigin: "anonymous",
preferArchive,
}) })
); );
@ -644,7 +675,7 @@ function useDownloadableImage(visibleLayers) {
); );
setDownloadImageUrl(canvas.toDataURL("image/png")); setDownloadImageUrl(canvas.toDataURL("image/png"));
setPreparedForLayerIds(layerIds); setPreparedForLayerIds(layerIds);
}, [preparedForLayerIds, visibleLayers, toast, hiResMode]); }, [preparedForLayerIds, visibleLayers, toast, hiResMode, preferArchive]);
return [downloadImageUrl, prepareDownload]; return [downloadImageUrl, prepareDownload];
} }

View file

@ -21,6 +21,7 @@ import Link from "next/link";
import SquareItemCard from "./SquareItemCard"; import SquareItemCard from "./SquareItemCard";
import { safeImageUrl, useCommonStyles } from "../util"; import { safeImageUrl, useCommonStyles } from "../util";
import usePreferArchive from "./usePreferArchive";
function ItemCard({ item, badges, variant = "list", ...props }) { function ItemCard({ item, badges, variant = "list", ...props }) {
const { brightBackground } = useCommonStyles(); const { brightBackground } = useCommonStyles();
@ -105,6 +106,7 @@ export function ItemThumbnail({
focusSelector, focusSelector,
...props ...props
}) { }) {
const [preferArchive] = usePreferArchive();
const theme = useTheme(); const theme = useTheme();
const borderColor = useColorModeValue( const borderColor = useColorModeValue(
@ -170,7 +172,7 @@ export function ItemThumbnail({
as="img" as="img"
width="100%" width="100%"
height="100%" height="100%"
src={safeImageUrl(item.thumbnailUrl)} src={safeImageUrl(item.thumbnailUrl, { preferArchive })}
alt={`Thumbnail art for ${item.name}`} alt={`Thumbnail art for ${item.name}`}
/> />
)} )}

View file

@ -3,6 +3,7 @@ import LRU from "lru-cache";
import { Box, Grid, useToast } from "@chakra-ui/react"; import { Box, Grid, useToast } from "@chakra-ui/react";
import { loadImage, logAndCapture, safeImageUrl } from "../util"; import { loadImage, logAndCapture, safeImageUrl } from "../util";
import usePreferArchive from "./usePreferArchive";
// Import EaselJS and TweenJS directly into the `window` object! The bundled // Import EaselJS and TweenJS directly into the `window` object! The bundled
// scripts are built to attach themselves to `window.createjs`, and // scripts are built to attach themselves to `window.createjs`, and
@ -24,6 +25,7 @@ function OutfitMovieLayer({
onLowFps = null, onLowFps = null,
canvasProps = {}, canvasProps = {},
}) { }) {
const [preferArchive] = usePreferArchive();
const [stage, setStage] = React.useState(null); const [stage, setStage] = React.useState(null);
const [library, setLibrary] = React.useState(null); const [library, setLibrary] = React.useState(null);
const [movieClip, setMovieClip] = React.useState(null); const [movieClip, setMovieClip] = React.useState(null);
@ -129,7 +131,7 @@ function OutfitMovieLayer({
React.useEffect(() => { React.useEffect(() => {
let canceled = false; let canceled = false;
const movieLibraryPromise = loadMovieLibrary(libraryUrl); const movieLibraryPromise = loadMovieLibrary(libraryUrl, { preferArchive });
movieLibraryPromise movieLibraryPromise
.then((library) => { .then((library) => {
if (canceled) { if (canceled) {
@ -154,7 +156,7 @@ function OutfitMovieLayer({
setLibrary(null); setLibrary(null);
setMovieClip(null); setMovieClip(null);
}; };
}, [libraryUrl, onError]); }, [libraryUrl, preferArchive, onError]);
// This effect puts the `movieClip` on the `stage`, when both are ready. // This effect puts the `movieClip` on the `stage`, when both are ready.
React.useEffect(() => { React.useEffect(() => {
@ -305,7 +307,7 @@ function loadScriptTag(src) {
const MOVIE_LIBRARY_CACHE = new LRU(10); const MOVIE_LIBRARY_CACHE = new LRU(10);
export function loadMovieLibrary(librarySrc) { export function loadMovieLibrary(librarySrc, { preferArchive = false } = {}) {
const cancelableResourcePromises = []; const cancelableResourcePromises = [];
const cancelAllResources = () => const cancelAllResources = () =>
cancelableResourcePromises.forEach((p) => p.cancel()); cancelableResourcePromises.forEach((p) => p.cancel());
@ -323,7 +325,9 @@ export function loadMovieLibrary(librarySrc) {
} }
// Then, load the script tag. (Make sure we set it up to be cancelable!) // Then, load the script tag. (Make sure we set it up to be cancelable!)
const scriptPromise = loadScriptTag(safeImageUrl(librarySrc)); const scriptPromise = loadScriptTag(
safeImageUrl(librarySrc, { preferArchive })
);
cancelableResourcePromises.push(scriptPromise); cancelableResourcePromises.push(scriptPromise);
await scriptPromise; await scriptPromise;
@ -372,6 +376,7 @@ export function loadMovieLibrary(librarySrc) {
id, id,
loadImage(librarySrcDir + "/" + src, { loadImage(librarySrcDir + "/" + src, {
crossOrigin: "anonymous", crossOrigin: "anonymous",
preferArchive,
}), }),
]) ])
); );

View file

@ -20,6 +20,7 @@ import OutfitMovieLayer, {
import HangerSpinner from "./HangerSpinner"; import HangerSpinner from "./HangerSpinner";
import { loadImage, safeImageUrl, useLocalStorage } from "../util"; import { loadImage, safeImageUrl, useLocalStorage } from "../util";
import useOutfitAppearance from "./useOutfitAppearance"; import useOutfitAppearance from "./useOutfitAppearance";
import usePreferArchive from "./usePreferArchive";
/** /**
* OutfitPreview is for rendering a full outfit! It accepts outfit data, * OutfitPreview is for rendering a full outfit! It accepts outfit data,
@ -169,6 +170,7 @@ export function OutfitLayers({
...props ...props
}) { }) {
const [hiResMode] = useLocalStorage("DTIHiResMode", false); const [hiResMode] = useLocalStorage("DTIHiResMode", false);
const [preferArchive] = usePreferArchive();
const containerRef = React.useRef(null); const containerRef = React.useRef(null);
const [canvasSize, setCanvasSize] = React.useState(0); const [canvasSize, setCanvasSize] = React.useState(0);
@ -281,7 +283,8 @@ export function OutfitLayers({
<Box <Box
as="img" as="img"
src={safeImageUrl( src={safeImageUrl(
getBestImageUrlForLayer(layer, { hiResMode }) getBestImageUrlForLayer(layer, { hiResMode }),
{ preferArchive }
)} )}
alt="" alt=""
objectFit="contain" objectFit="contain"
@ -367,6 +370,7 @@ export function getBestImageUrlForLayer(layer, { hiResMode = false } = {}) {
*/ */
export function usePreloadLayers(layers) { export function usePreloadLayers(layers) {
const [hiResMode] = useLocalStorage("DTIHiResMode", false); const [hiResMode] = useLocalStorage("DTIHiResMode", false);
const [preferArchive] = usePreferArchive();
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
const [loadedLayers, setLoadedLayers] = React.useState([]); const [loadedLayers, setLoadedLayers] = React.useState([]);
@ -393,7 +397,8 @@ export function usePreloadLayers(layers) {
const movieAssetPromises = []; const movieAssetPromises = [];
for (const layer of layers) { for (const layer of layers) {
const imageAssetPromise = loadImage( const imageAssetPromise = loadImage(
getBestImageUrlForLayer(layer, { hiResMode }) getBestImageUrlForLayer(layer, { hiResMode }),
{ preferArchive }
); );
imageAssetPromises.push(imageAssetPromise); imageAssetPromises.push(imageAssetPromise);
@ -402,7 +407,8 @@ export function usePreloadLayers(layers) {
// request will still be the image, which we'll show as a // request will still be the image, which we'll show as a
// placeholder, which should usually be noticeably faster! // placeholder, which should usually be noticeably faster!
const movieLibraryPromise = loadMovieLibrary( const movieLibraryPromise = loadMovieLibrary(
layer.canvasMovieLibraryUrl layer.canvasMovieLibraryUrl,
{ preferArchive }
); );
const movieAssetPromise = movieLibraryPromise.then((library) => ({ const movieAssetPromise = movieLibraryPromise.then((library) => ({
library, library,
@ -465,7 +471,7 @@ export function usePreloadLayers(layers) {
return () => { return () => {
canceled = true; canceled = true;
}; };
}, [layers, hiResMode]); }, [layers, hiResMode, preferArchive]);
return { loading, error, loadedLayers, layersHaveAnimations }; return { loading, error, loadedLayers, layersHaveAnimations };
} }

View file

@ -12,6 +12,7 @@ import Link from "next/link";
import { safeImageUrl, useCommonStyles } from "../util"; import { safeImageUrl, useCommonStyles } from "../util";
import { CheckIcon, CloseIcon, StarIcon } from "@chakra-ui/icons"; import { CheckIcon, CloseIcon, StarIcon } from "@chakra-ui/icons";
import usePreferArchive from "./usePreferArchive";
function SquareItemCard({ function SquareItemCard({
item, item,
@ -183,6 +184,7 @@ function SquareItemCardLayout({
} }
function ItemThumbnail({ item, tradeMatchingMode }) { function ItemThumbnail({ item, tradeMatchingMode }) {
const [preferArchive] = usePreferArchive();
const kindColorScheme = item.isNc ? "purple" : item.isPb ? "orange" : "gray"; const kindColorScheme = item.isNc ? "purple" : item.isPb ? "orange" : "gray";
const thumbnailShadowColor = useColorModeValue( const thumbnailShadowColor = useColorModeValue(
@ -229,7 +231,7 @@ function ItemThumbnail({ item, tradeMatchingMode }) {
> >
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={safeImageUrl(item.thumbnailUrl)} src={safeImageUrl(item.thumbnailUrl, { preferArchive })}
alt={`Thumbnail art for ${item.name}`} alt={`Thumbnail art for ${item.name}`}
width={80} width={80}
height={80} height={80}

View file

@ -0,0 +1,22 @@
import { useLocalStorage } from "../util";
/**
* usePreferArchive helps the user choose to try using our archive before
* using images.neopets.com, when images.neopets.com is being slow and bleh!
*/
function usePreferArchive() {
const [preferArchiveSavedValue, setPreferArchive] = useLocalStorage(
"DTIPreferArchive",
null
);
// Oct 13 2022: I might default this back to on again if the lag gets
// miserable again, but it's okaaay right now? ish? Bad enough that I want to
// offer this option, but decent enough that I don't want to turn it on by
// default and break new items yet!
const preferArchive = preferArchiveSavedValue ?? false;
return [preferArchive, setPreferArchive];
}
export default usePreferArchive;

View file

@ -115,7 +115,10 @@ export function useCommonStyles() {
/** /**
* safeImageUrl returns an HTTPS-safe image URL for Neopets assets! * safeImageUrl returns an HTTPS-safe image URL for Neopets assets!
*/ */
export function safeImageUrl(urlString, { crossOrigin = null } = {}) { export function safeImageUrl(
urlString,
{ crossOrigin = null, preferArchive = false } = {}
) {
if (urlString == null) { if (urlString == null) {
return urlString; return urlString;
} }
@ -148,7 +151,14 @@ export function safeImageUrl(urlString, { crossOrigin = null } = {}) {
url.origin === "https://images.neopets.com" url.origin === "https://images.neopets.com"
) { ) {
url.protocol = "https:"; url.protocol = "https:";
if (crossOrigin) { if (preferArchive) {
const archiveUrl = new URL(
`/api/readFromArchive`,
window.location.origin
);
archiveUrl.search = new URLSearchParams({ url: url.toString() });
url = archiveUrl;
} else if (crossOrigin) {
url.host = "images.neopets-asset-proxy.openneo.net"; url.host = "images.neopets-asset-proxy.openneo.net";
} }
} else if ( } else if (
@ -161,7 +171,7 @@ export function safeImageUrl(urlString, { crossOrigin = null } = {}) {
} }
} }
if (url.protocol !== "https:") { if (url.protocol !== "https:" && url.hostname !== "localhost") {
logAndCapture( logAndCapture(
new Error( new Error(
`safeImageUrl was provided an unsafe URL, but we don't know how to ` + `safeImageUrl was provided an unsafe URL, but we don't know how to ` +
@ -337,8 +347,11 @@ export function useLocalStorage(key, initialValue) {
return [storedValue, setValue]; return [storedValue, setValue];
} }
export function loadImage(rawSrc, { crossOrigin = null } = {}) { export function loadImage(
const src = safeImageUrl(rawSrc, { crossOrigin }); rawSrc,
{ crossOrigin = null, preferArchive = false } = {}
) {
const src = safeImageUrl(rawSrc, { crossOrigin, preferArchive });
const image = new Image(); const image = new Image();
let canceled = false; let canceled = false;
let resolved = false; let resolved = false;