diff --git a/packages/skin-database/api/graphql/resolvers/SkinResolver.ts b/packages/skin-database/api/graphql/resolvers/SkinResolver.ts index 849df8f3..784af828 100644 --- a/packages/skin-database/api/graphql/resolvers/SkinResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/SkinResolver.ts @@ -4,14 +4,9 @@ import UserContext from "../../../data/UserContext"; import ClassicSkinResolver from "./ClassicSkinResolver"; import { ISkin } from "./CommonSkinResolver"; import ModernSkinResolver from "./ModernSkinResolver"; -import algoliasearch from "algoliasearch"; import * as Skins from "../../../data/skins"; import { knex } from "../../../db"; -// These keys are already in the web client, so they are not secret at all. -const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51"); -const index = client.initIndex("Skins"); - export default class SkinResolver { constructor() { throw new Error("This is a stub."); diff --git a/packages/skin-database/legacy-client/src/algolia.js b/packages/skin-database/legacy-client/src/algolia.js index 363069f8..90db2cec 100644 --- a/packages/skin-database/legacy-client/src/algolia.js +++ b/packages/skin-database/legacy-client/src/algolia.js @@ -1,12 +1,11 @@ import * as Utils from "./utils"; -import algoliasearch from "algoliasearch"; -var client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51"); +import { algoliasearch } from "algoliasearch"; -var index = client.initIndex("Skins"); +const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51"); // Fallback search that uses SQLite. Useful for when we've exceeded the Algolia // search quota. -export async function graphqlSearch(query, options = {}) { +export async function graphqlSearch(query) { const queryText = Utils.gql` query SearchQuery($query: String!) { search_classic_skins(query: $query, first: 500) { @@ -26,27 +25,19 @@ export async function graphqlSearch(query, options = {}) { return { hits }; } -export function algoliaSearch(query, options = {}) { - console.log("algoliaSearch", query, options); - return new Promise((resolve, reject) => { - index.search( - { - query, - attributes: ["objectID", "fileName", "color", "nsfw"], - attributesToHighlight: [], - hitsPerPage: 1000, - // https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/ - // min: Retrieve records with the smallest number of typos. - typoTolerance: "min", - ...options, - }, - (err, content) => { - if (err != null) { - reject(err); - return; - } - resolve(content); - } - ); +export async function algoliaSearch(query, options = {}) { + const result = await client.searchSingleIndex({ + indexName: "Skins", + searchParams: { + query, + attributesToRetrieve: ["objectID", "fileName", "color", "nsfw"], + attributesToHighlight: [], + hitsPerPage: 1000, + // https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/ + // min: Retrieve records with the smallest number of typos. + typoTolerance: "min", + ...options, + }, }); + return result; } diff --git a/packages/skin-database/legacy-client/src/components/DownloadLink.tsx b/packages/skin-database/legacy-client/src/components/DownloadLink.tsx new file mode 100644 index 00000000..ff409a9a --- /dev/null +++ b/packages/skin-database/legacy-client/src/components/DownloadLink.tsx @@ -0,0 +1,65 @@ +import React, { useState, useEffect } from "react"; + +// The `download` attribute on `` tags is not respected on cross origin +// assets. However, it does work for Object URLs. So, we download the skin as +// soon as we show the link and swap out the href with the Object URL as soon as +// it's loaded. The skin should already be cached, so it should not actually +// result in an extra network request. +// +// There may be a brief time where the download link will use the hash name instead +// of the real name, but it's probably too short to actually ever be hit by a real user. + +interface DownloadLinkProps + extends React.AnchorHTMLAttributes { + href: string; + children: React.ReactNode; +} + +const DownloadLink: React.FC = ({ + href: originalHref, + children, + ...props +}) => { + const [objectUrl, setObjectUrl] = useState(null); + + useEffect(() => { + let isCancelled = false; + + const fetchAndCreateObjectUrl = async (href: string) => { + try { + const response = await fetch(href); + const blob = await response.blob(); + + if (!isCancelled) { + const url = URL.createObjectURL(blob); + setObjectUrl(url); + } + } catch (error) { + console.error("Failed to fetch and create object URL:", error); + } + }; + + fetchAndCreateObjectUrl(originalHref); + + return () => { + isCancelled = true; + }; + }, [originalHref]); + + // Separate effect to clean up object URLs when they change or component unmounts + useEffect(() => { + return () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [objectUrl]); + + return ( + + {children} + + ); +}; + +export default DownloadLink; diff --git a/packages/skin-database/legacy-client/src/components/Metadata.js b/packages/skin-database/legacy-client/src/components/Metadata.js index 63fcd0cb..9c74f915 100644 --- a/packages/skin-database/legacy-client/src/components/Metadata.js +++ b/packages/skin-database/legacy-client/src/components/Metadata.js @@ -9,6 +9,7 @@ import { useSelector } from "react-redux"; // import { useActionCreator } from "../hooks"; import DownloadText from "./DownloadText"; import { useActionCreator } from "../hooks"; +import DownloadLink from "./DownloadLink"; function Metadata() { const hash = useSelector(Selectors.getSelectedSkinHash); @@ -58,13 +59,13 @@ function Metadata() { } const elements = [ - Download - , + , readmeLink, /*