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 useOutfitAppearance from "../components/useOutfitAppearance";
import OutfitKnownGlitchesBadge from "./OutfitKnownGlitchesBadge";
import usePreferArchive from "../components/usePreferArchive";
/**
* OutfitControls is the set of controls layered over the outfit preview, to
@ -500,28 +501,56 @@ function SettingsButton({ onLockFocus, onUnlockFocus }) {
function HiResModeSetting() {
const [hiResMode, setHiResMode] = useLocalStorage("DTIHiResMode", false);
const [preferArchive, setPreferArchive] = usePreferArchive();
return (
<FormControl>
<Flex>
<Box>
<FormLabel htmlFor="hi-res-mode-setting" fontSize="sm" margin="0">
Hi-res mode (SVG)
</FormLabel>
<FormHelperText marginTop="0" fontSize="xs">
Crisper at higher resolutions, but not always accurate
</FormHelperText>
</Box>
<Box width="2" />
<Switch
id="hi-res-mode-setting"
size="sm"
marginTop="0.1rem"
isChecked={hiResMode}
onChange={(e) => setHiResMode(e.target.checked)}
/>
</Flex>
</FormControl>
<Box>
<FormControl>
<Flex>
<Box>
<FormLabel htmlFor="hi-res-mode-setting" fontSize="sm" margin="0">
Hi-res mode (SVG)
</FormLabel>
<FormHelperText marginTop="0" fontSize="xs">
Crisper at higher resolutions, but not always accurate
</FormHelperText>
</Box>
<Box width="2" />
<Switch
id="hi-res-mode-setting"
size="sm"
marginTop="0.1rem"
isChecked={hiResMode}
onChange={(e) => setHiResMode(e.target.checked)}
/>
</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) {
const [hiResMode] = useLocalStorage("DTIHiResMode", false);
const [preferArchive] = usePreferArchive();
const [downloadImageUrl, setDownloadImageUrl] = React.useState(null);
const [preparedForLayerIds, setPreparedForLayerIds] = React.useState([]);
@ -611,6 +641,7 @@ function useDownloadableImage(visibleLayers) {
const imagePromises = visibleLayers.map((layer) =>
loadImage(getBestImageUrlForLayer(layer, { hiResMode }), {
crossOrigin: "anonymous",
preferArchive,
})
);
@ -644,7 +675,7 @@ function useDownloadableImage(visibleLayers) {
);
setDownloadImageUrl(canvas.toDataURL("image/png"));
setPreparedForLayerIds(layerIds);
}, [preparedForLayerIds, visibleLayers, toast, hiResMode]);
}, [preparedForLayerIds, visibleLayers, toast, hiResMode, preferArchive]);
return [downloadImageUrl, prepareDownload];
}

View file

@ -21,6 +21,7 @@ import Link from "next/link";
import SquareItemCard from "./SquareItemCard";
import { safeImageUrl, useCommonStyles } from "../util";
import usePreferArchive from "./usePreferArchive";
function ItemCard({ item, badges, variant = "list", ...props }) {
const { brightBackground } = useCommonStyles();
@ -105,6 +106,7 @@ export function ItemThumbnail({
focusSelector,
...props
}) {
const [preferArchive] = usePreferArchive();
const theme = useTheme();
const borderColor = useColorModeValue(
@ -170,7 +172,7 @@ export function ItemThumbnail({
as="img"
width="100%"
height="100%"
src={safeImageUrl(item.thumbnailUrl)}
src={safeImageUrl(item.thumbnailUrl, { preferArchive })}
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 { loadImage, logAndCapture, safeImageUrl } from "../util";
import usePreferArchive from "./usePreferArchive";
// Import EaselJS and TweenJS directly into the `window` object! The bundled
// scripts are built to attach themselves to `window.createjs`, and
@ -24,6 +25,7 @@ function OutfitMovieLayer({
onLowFps = null,
canvasProps = {},
}) {
const [preferArchive] = usePreferArchive();
const [stage, setStage] = React.useState(null);
const [library, setLibrary] = React.useState(null);
const [movieClip, setMovieClip] = React.useState(null);
@ -129,7 +131,7 @@ function OutfitMovieLayer({
React.useEffect(() => {
let canceled = false;
const movieLibraryPromise = loadMovieLibrary(libraryUrl);
const movieLibraryPromise = loadMovieLibrary(libraryUrl, { preferArchive });
movieLibraryPromise
.then((library) => {
if (canceled) {
@ -154,7 +156,7 @@ function OutfitMovieLayer({
setLibrary(null);
setMovieClip(null);
};
}, [libraryUrl, onError]);
}, [libraryUrl, preferArchive, onError]);
// This effect puts the `movieClip` on the `stage`, when both are ready.
React.useEffect(() => {
@ -305,7 +307,7 @@ function loadScriptTag(src) {
const MOVIE_LIBRARY_CACHE = new LRU(10);
export function loadMovieLibrary(librarySrc) {
export function loadMovieLibrary(librarySrc, { preferArchive = false } = {}) {
const cancelableResourcePromises = [];
const cancelAllResources = () =>
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!)
const scriptPromise = loadScriptTag(safeImageUrl(librarySrc));
const scriptPromise = loadScriptTag(
safeImageUrl(librarySrc, { preferArchive })
);
cancelableResourcePromises.push(scriptPromise);
await scriptPromise;
@ -372,6 +376,7 @@ export function loadMovieLibrary(librarySrc) {
id,
loadImage(librarySrcDir + "/" + src, {
crossOrigin: "anonymous",
preferArchive,
}),
])
);

View file

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

View file

@ -12,6 +12,7 @@ import Link from "next/link";
import { safeImageUrl, useCommonStyles } from "../util";
import { CheckIcon, CloseIcon, StarIcon } from "@chakra-ui/icons";
import usePreferArchive from "./usePreferArchive";
function SquareItemCard({
item,
@ -183,6 +184,7 @@ function SquareItemCardLayout({
}
function ItemThumbnail({ item, tradeMatchingMode }) {
const [preferArchive] = usePreferArchive();
const kindColorScheme = item.isNc ? "purple" : item.isPb ? "orange" : "gray";
const thumbnailShadowColor = useColorModeValue(
@ -229,7 +231,7 @@ function ItemThumbnail({ item, tradeMatchingMode }) {
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={safeImageUrl(item.thumbnailUrl)}
src={safeImageUrl(item.thumbnailUrl, { preferArchive })}
alt={`Thumbnail art for ${item.name}`}
width={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!
*/
export function safeImageUrl(urlString, { crossOrigin = null } = {}) {
export function safeImageUrl(
urlString,
{ crossOrigin = null, preferArchive = false } = {}
) {
if (urlString == null) {
return urlString;
}
@ -148,7 +151,14 @@ export function safeImageUrl(urlString, { crossOrigin = null } = {}) {
url.origin === "https://images.neopets.com"
) {
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";
}
} else if (
@ -161,7 +171,7 @@ export function safeImageUrl(urlString, { crossOrigin = null } = {}) {
}
}
if (url.protocol !== "https:") {
if (url.protocol !== "https:" && url.hostname !== "localhost") {
logAndCapture(
new Error(
`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];
}
export function loadImage(rawSrc, { crossOrigin = null } = {}) {
const src = safeImageUrl(rawSrc, { crossOrigin });
export function loadImage(
rawSrc,
{ crossOrigin = null, preferArchive = false } = {}
) {
const src = safeImageUrl(rawSrc, { crossOrigin, preferArchive });
const image = new Image();
let canceled = false;
let resolved = false;