Add option to use img tag to fetch images

MangaUpdates covers could not be fetched via the fetch api because there were cors issues
This commit is contained in:
schroda
2024-03-28 22:40:55 +01:00
parent 816cbee145
commit 74cf4b7c50
3 changed files with 62 additions and 11 deletions

View File

@@ -145,6 +145,7 @@ export const TrackerMangaCard = ({
>
<TrackerMangaCardLink url={manga.trackingUrl}>
<SpinnerImage
useFetchApi={false}
alt={manga.title}
src={manga.coverUrl}
spinnerStyle={{ width: '100%', height: '100%' }}

View File

@@ -27,10 +27,12 @@ interface IProps {
imgStyle?: CSSProperties;
onImageLoad?: () => void;
useFetchApi?: boolean;
}
export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTMLImageElement | null>) => {
const { src, alt, onImageLoad, spinnerStyle: { small, ...spinnerStyle } = {}, imgStyle } = props;
const { useFetchApi, src, alt, onImageLoad, spinnerStyle: { small, ...spinnerStyle } = {}, imgStyle } = props;
const { t } = useTranslation();
@@ -55,8 +57,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
return () => {};
}
let tmpImageSourceUrl: string;
const imageRequest = requestManager.requestImage(src, Priority.HIGH);
const imageRequest = requestManager.requestImage(src, Priority.HIGH, useFetchApi);
let cacheTimeout: NodeJS.Timeout;
const fetchImage = async () => {
@@ -66,7 +67,6 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
updateImageState(false);
setImageSourceUrl(image);
tmpImageSourceUrl = image;
};
const checkCache = await Promise.race([
@@ -94,9 +94,7 @@ export const SpinnerImage = forwardRef((props: IProps, imgRef: ForwardedRef<HTML
fetchImage().catch(defaultPromiseErrorHandler);
return () => {
if (tmpImageSourceUrl) {
URL.revokeObjectURL(tmpImageSourceUrl);
}
imageRequest.cleanup();
clearTimeout(cacheTimeout);
imageRequest.abortRequest(new Error('Component was unmounted'));
};

View File

@@ -280,6 +280,7 @@ import {
TRACKER_LOGOUT,
TRACKER_UPDATE_BIND,
} from '@/lib/graphql/mutations/TrackerMutation.ts';
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
enum GQLMethod {
QUERY = 'QUERY',
@@ -330,6 +331,8 @@ type SubscriptionHookOptions<Data = any, Variables extends OperationVariables =
type AbortableRequest = { abortRequest: AbortController['abort'] };
type ImageRequest = { response: Promise<string>; cleanup: () => void } & AbortableRequest;
export type AbortabaleApolloQueryResponse<Data = any> = {
response: Promise<ApolloQueryResult<Data>>;
} & AbortableRequest;
@@ -778,6 +781,39 @@ export class RequestManager {
return `${this.getValidUrlFor(imageUrl, apiVersion)}`;
}
private fetchImageViaTag(url: string, priority?: QueuePriority): ImageRequest {
const imgRequest = new ControlledPromise<string>();
imgRequest.promise.catch(defaultPromiseErrorHandler(`fetchImageViaTag(${url})`));
const img = new Image();
const abortRequest = (reason?: any) => {
img.src = '';
img.onload = null;
img.onerror = null;
img.onabort = null;
imgRequest.reject(reason);
};
const response = this.imageQueue.enqueue(
url,
async () => {
// throws error in case request was already aborted
await Promise.race([imgRequest.promise, Promise.resolve()]);
img.src = url;
img.onload = () => imgRequest.resolve(url);
img.onerror = (error) => imgRequest.reject(error);
img.onabort = (error) => imgRequest.reject(error);
return imgRequest.promise;
},
priority,
);
return { response, abortRequest, cleanup: () => {} };
}
/**
* After the image has been handled, {@see URL#revokeObjectURL} has to be called.
*
@@ -787,11 +823,12 @@ export class RequestManager {
* const imageUrl = await imageRequest.response
*
* const img = new Image();
* img.onLoad = () => URL.revokeObjectURL(imageUrl);
* img.onLoad = () => imageRequest.cleanup();
* img.src = imageUrl;
*
*/
public requestImage(url: string, priority?: QueuePriority): { response: Promise<string> } & AbortableRequest {
private fetchImageViaFetchApi(url: string, priority?: QueuePriority): ImageRequest {
let objectUrl: string = '';
const { abortRequest, signal } = this.createAbortController();
const response = this.imageQueue.enqueue(
url,
@@ -806,11 +843,26 @@ export class RequestManager {
},
})
.then((data) => data.blob())
.then((data) => URL.createObjectURL(data)),
.then((data) => URL.createObjectURL(data))
.then((imageUrl) => {
objectUrl = imageUrl;
return imageUrl;
}),
priority,
);
return { response, abortRequest };
return { response, abortRequest, cleanup: () => URL.revokeObjectURL(objectUrl) };
}
/**
* Make sure to call "cleanup" once the image is not needed anymore (only required if fetched via "fetch api")
*/
public requestImage(url: string, priority?: QueuePriority, useFetchApi: boolean = true): ImageRequest {
if (useFetchApi) {
return this.fetchImageViaFetchApi(url, priority);
}
return this.fetchImageViaTag(url, priority);
}
private doRequest<Data, Variables extends OperationVariables = OperationVariables>(