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}> <TrackerMangaCardLink url={manga.trackingUrl}>
<SpinnerImage <SpinnerImage
useFetchApi={false}
alt={manga.title} alt={manga.title}
src={manga.coverUrl} src={manga.coverUrl}
spinnerStyle={{ width: '100%', height: '100%' }} spinnerStyle={{ width: '100%', height: '100%' }}

View File

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

View File

@@ -280,6 +280,7 @@ import {
TRACKER_LOGOUT, TRACKER_LOGOUT,
TRACKER_UPDATE_BIND, TRACKER_UPDATE_BIND,
} from '@/lib/graphql/mutations/TrackerMutation.ts'; } from '@/lib/graphql/mutations/TrackerMutation.ts';
import { ControlledPromise } from '@/lib/ControlledPromise.ts';
enum GQLMethod { enum GQLMethod {
QUERY = 'QUERY', QUERY = 'QUERY',
@@ -330,6 +331,8 @@ type SubscriptionHookOptions<Data = any, Variables extends OperationVariables =
type AbortableRequest = { abortRequest: AbortController['abort'] }; type AbortableRequest = { abortRequest: AbortController['abort'] };
type ImageRequest = { response: Promise<string>; cleanup: () => void } & AbortableRequest;
export type AbortabaleApolloQueryResponse<Data = any> = { export type AbortabaleApolloQueryResponse<Data = any> = {
response: Promise<ApolloQueryResult<Data>>; response: Promise<ApolloQueryResult<Data>>;
} & AbortableRequest; } & AbortableRequest;
@@ -778,6 +781,39 @@ export class RequestManager {
return `${this.getValidUrlFor(imageUrl, apiVersion)}`; 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. * 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 imageUrl = await imageRequest.response
* *
* const img = new Image(); * const img = new Image();
* img.onLoad = () => URL.revokeObjectURL(imageUrl); * img.onLoad = () => imageRequest.cleanup();
* img.src = imageUrl; * 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 { abortRequest, signal } = this.createAbortController();
const response = this.imageQueue.enqueue( const response = this.imageQueue.enqueue(
url, url,
@@ -806,11 +843,26 @@ export class RequestManager {
}, },
}) })
.then((data) => data.blob()) .then((data) => data.blob())
.then((data) => URL.createObjectURL(data)), .then((data) => URL.createObjectURL(data))
.then((imageUrl) => {
objectUrl = imageUrl;
return imageUrl;
}),
priority, 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>( private doRequest<Data, Variables extends OperationVariables = OperationVariables>(