-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathdisplayed-image.tsx
230 lines (195 loc) · 8.55 KB
/
displayed-image.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import { LOGGER } from '@lib/logging/logger';
import { assertDefined, assertNonNull } from '@lib/util/assert';
import { logFailure } from '@lib/util/async';
import { qs, qsMaybe } from '@lib/util/dom';
import { formatFileSize } from '@lib/util/format';
import type { ImageInfo } from './image-info';
import type { InfoCache } from './info-cache';
import { CAAImage, QueuedUploadImage } from './image';
export interface DisplayedImage {
readonly imageElement: HTMLImageElement;
loadAndDisplay(): Promise<void>;
}
export function createDimensionsString(imageInfo: ImageInfo): string {
return (imageInfo.dimensions !== undefined
? `${imageInfo.dimensions.width}x${imageInfo.dimensions.height}`
: 'failed :(');
}
export function createFileInfoString(imageInfo: ImageInfo): string {
const details: string[] = [];
if (imageInfo.size !== undefined) {
details.push(formatFileSize(imageInfo.size));
}
if (imageInfo.fileType !== undefined) {
details.push(imageInfo.fileType);
}
if (imageInfo.pageCount !== undefined) {
details.push(imageInfo.pageCount.toString() + (imageInfo.pageCount === 1 ? ' page' : ' pages'));
}
return details.join(', ');
}
abstract class BaseDisplayedImage implements DisplayedImage {
public readonly imageElement: HTMLImageElement;
private readonly _labelPlacementAnchor: Element;
private _dimensionsSpan: HTMLSpanElement | null = null;
private _fileInfoSpan: HTMLSpanElement | null = null;
public constructor(imageElement: HTMLImageElement, labelPlacementAnchor?: Element | null) {
this._labelPlacementAnchor = labelPlacementAnchor ?? imageElement;
this.imageElement = imageElement;
}
protected get dimensionsSpan(): HTMLSpanElement {
if (this._dimensionsSpan !== null) return this._dimensionsSpan;
// Possibly already added previously. Shouldn't happen within this script,
// but can happen in Supercharged CAA Edits.
this._dimensionsSpan = qsMaybe<HTMLSpanElement>('span.ROpdebee_dimensions', this.imageElement.parentElement!);
if (this._dimensionsSpan !== null) return this._dimensionsSpan;
// First time accessing the dimensions, add it now.
this._dimensionsSpan = <span className="ROpdebee_dimensions"></span>;
this._labelPlacementAnchor.insertAdjacentElement('afterend', this._dimensionsSpan);
return this._dimensionsSpan;
}
protected get fileInfoSpan(): HTMLSpanElement {
if (this._fileInfoSpan !== null) return this._fileInfoSpan;
this._fileInfoSpan = qsMaybe<HTMLSpanElement>('span.ROpdebee_fileInfo', this.imageElement.parentElement!);
if (this._fileInfoSpan !== null) return this._fileInfoSpan;
this._fileInfoSpan = <span className="ROpdebee_fileInfo"></span>;
this.dimensionsSpan.insertAdjacentElement('afterend', this._fileInfoSpan);
return this._fileInfoSpan;
}
public abstract loadAndDisplay(): Promise<void>;
}
abstract class DisplayedCAAImage extends BaseDisplayedImage {
private readonly image: CAAImage;
public constructor(imageElement: HTMLImageElement, image: CAAImage) {
super(imageElement);
this.image = image;
}
public async loadAndDisplay(): Promise<void> {
// Don't load dimensions if it's already loaded/currently being loaded
if (this.imageElement.getAttribute('ROpdebee_lazyDimensions')) {
return;
}
this.displayInfo('pending…');
try {
const imageInfo = await this.image.getImageInfo();
this.displayInfo(this.createDimensionsString(imageInfo), this.createFileInfoString(imageInfo));
} catch (error) {
LOGGER.error('Failed to load image information', error);
this.displayInfo('failed :(');
}
}
protected displayInfo(dimensionsString: string, fileInfoString?: string): void {
this.imageElement.setAttribute('ROpdebee_lazyDimensions', dimensionsString);
this.dimensionsSpan.textContent = dimensionsString;
if (fileInfoString !== undefined) {
this.fileInfoSpan.textContent = fileInfoString;
}
}
protected createDimensionsString(imageInfo: ImageInfo): string {
return `Dimensions: ${createDimensionsString(imageInfo)}`;
}
protected createFileInfoString(imageInfo: ImageInfo): string | undefined {
const detailsString = createFileInfoString(imageInfo);
if (detailsString) {
return detailsString;
}
return undefined;
}
}
/**
* CAA images contained within an anchor element with `artwork-image` class.
*
* The full-size URL is the `href` of that anchor.
*/
export class ArtworkImageAnchorCAAImage extends DisplayedCAAImage {
public constructor(imageElement: HTMLImageElement, cache: InfoCache) {
const fullSizeUrl = imageElement.closest<HTMLAnchorElement>('a.artwork-image, a.artwork-pdf')?.href;
assertDefined(fullSizeUrl);
super(imageElement, new CAAImage(fullSizeUrl, cache, imageElement.src));
}
}
/**
* CAA images on the cover art tab.
*
* Full-size URL needs to be retrieved from the anchors below the image.
*/
export class CoverArtTabCAAImage extends DisplayedCAAImage {
public constructor(imageElement: HTMLImageElement, cache: InfoCache) {
const container = imageElement.closest('div.artwork-cont');
assertNonNull(container);
const fullSizeUrl = qs<HTMLAnchorElement>('p.small > a:last-of-type', container).href;
super(imageElement, new CAAImage(fullSizeUrl, cache));
}
}
/**
* CAA images with a `fullSizeURL` property.
*
* Intended for backward compatibility with other scripts.
*/
export class CAAImageWithFullSizeURL extends DisplayedCAAImage {
public constructor(imageElement: HTMLImageElement, cache: InfoCache) {
const fullSizeUrl = imageElement.getAttribute('fullSizeURL');
assertNonNull(fullSizeUrl);
super(imageElement, new CAAImage(fullSizeUrl, cache));
}
}
/**
* Like `ArtworkImageAnchorCAAImage`, but shorter info string.
*/
export class ThumbnailCAAImage extends ArtworkImageAnchorCAAImage {
protected override createDimensionsString(imageInfo: ImageInfo): string {
return createDimensionsString(imageInfo);
}
}
export class DisplayedQueuedUploadImage extends BaseDisplayedImage {
private readonly image: QueuedUploadImage;
// No cache, unnecessary to cache.
public constructor(imageElement: HTMLImageElement) {
super(imageElement, imageElement.parentElement?.lastElementChild);
this.image = new QueuedUploadImage(imageElement);
}
public async loadAndDisplay(): Promise<void> {
// Don't display on PDF images
if (this.imageElement.src.endsWith('/static/images/icons/pdf-icon.png')) return;
const dimensions = await this.image.getDimensions();
const infoString = `${dimensions.width}x${dimensions.height}`;
this.dimensionsSpan.textContent = infoString;
}
}
export function displayedCoverArtFactory(image: HTMLImageElement, cache: InfoCache): DisplayedImage | undefined {
try {
if (image.closest('.artwork-cont') !== null) { // Release cover art tab
return new CoverArtTabCAAImage(image, cache);
} else if (image.closest('.thumb-position') !== null || image.closest('form#set-cover-art') !== null) { // Add cover art page, existing images; set-cover-art pages for RG
return new ThumbnailCAAImage(image, cache);
} else {
return new ArtworkImageAnchorCAAImage(image, cache);
}
} catch (error) {
LOGGER.error('Failed to process image', error);
return undefined;
}
}
export const displayInfoWhenInView = ((): ((image: DisplayedImage) => void) => {
// Map image src to DisplayedImage instances. We'll retrieve from this map
// when the image scrolls into view.
const imageMap = new Map<HTMLImageElement, DisplayedImage>();
function inViewCallback(entries: IntersectionObserverEntry[]): void {
for (const entry of entries) {
if (entry.intersectionRatio <= 0) continue;
const image = imageMap.get(entry.target as HTMLImageElement)!;
image.loadAndDisplay().catch(logFailure('Failed to process image'));
}
}
const observer = new IntersectionObserver(inViewCallback, {
root: document,
});
return (image) => {
if (imageMap.has(image.imageElement)) {
// Already observing
return;
}
imageMap.set(image.imageElement, image);
observer.observe(image.imageElement);
};
})();