diff --git a/package-lock.json b/package-lock.json index 96b955bc6..ad2815396 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT; https://opensource.org/licenses/MIT", "dependencies": { "idb": "^7.0.1", + "jquery-ui": "^1.13.1", "p-retry": "^5.1.1", "p-throttle": "5.0.0", "ts-custom-error": "3.2.0" @@ -39,7 +40,10 @@ "@types/greasemonkey": "4.0.3", "@types/jest": "28.1.1", "@types/jest-when": "3.5.0", + "@types/jquery": "^3.5.14", + "@types/jqueryui": "^1.12.16", "@types/postcss-preset-env": "6.7.3", + "@types/resemblejs": "^4.1.0", "@types/rollup__plugin-virtual": "2.0.1", "@types/rollup-plugin-progress": "1.1.1", "@types/setup-polly-jest": "0.5.1", @@ -81,6 +85,7 @@ "jest-html-reporters": "3.0.9", "jest-when": "3.5.1", "license-checker": "25.0.1", + "moment": "^2.29.3", "nativejsx": "github:ROpdebee/nativejsx", "node-fetch": "3.2.6", "postcss": "8.4.14", @@ -3567,6 +3572,24 @@ "@types/jest": "*" } }, + "node_modules/@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/jqueryui": { + "version": "1.12.16", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.16.tgz", + "integrity": "sha512-6huAQDpNlso9ayaUT9amBOA3kj02OCeUWs+UvDmbaJmwkHSg/HLsQOoap/D5uveN9ePwl72N45Bl+Frp5xyG1Q==", + "dev": true, + "dependencies": { + "@types/jquery": "*" + } + }, "node_modules/@types/jsdom": { "version": "16.2.14", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz", @@ -3693,6 +3716,12 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "node_modules/@types/resemblejs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/resemblejs/-/resemblejs-4.1.0.tgz", + "integrity": "sha512-+MIkKy/UngDfhTnvn2yK/KSzlbtLeB5BU73qqZrzIF24+e2r8enJ4cW3UbtkstByYSDV8pbheGAqg7zT8ZZ2pA==", + "dev": true + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3790,6 +3819,12 @@ "@types/pollyjs__core": "*" } }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -9886,6 +9921,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "node_modules/jquery-ui": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.1.tgz", + "integrity": "sha512-2VlU59N5P4HaumDK1Z3XEVjSvegFbEOQRgpHUBaB2Ak98Axl3hFhJ6RFcNQNuk9SfL6WxIbuLst8dW/U56NSiA==", + "dependencies": { + "jquery": ">=1.8.0 <4.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10559,6 +10607,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -17199,6 +17256,24 @@ "@types/jest": "*" } }, + "@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/jqueryui": { + "version": "1.12.16", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.16.tgz", + "integrity": "sha512-6huAQDpNlso9ayaUT9amBOA3kj02OCeUWs+UvDmbaJmwkHSg/HLsQOoap/D5uveN9ePwl72N45Bl+Frp5xyG1Q==", + "dev": true, + "requires": { + "@types/jquery": "*" + } + }, "@types/jsdom": { "version": "16.2.14", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz", @@ -17320,6 +17395,12 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "@types/resemblejs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/resemblejs/-/resemblejs-4.1.0.tgz", + "integrity": "sha512-+MIkKy/UngDfhTnvn2yK/KSzlbtLeB5BU73qqZrzIF24+e2r8enJ4cW3UbtkstByYSDV8pbheGAqg7zT8ZZ2pA==", + "dev": true + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -17409,6 +17490,12 @@ "@types/pollyjs__core": "*" } }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -21863,6 +21950,19 @@ } } }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "jquery-ui": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.1.tgz", + "integrity": "sha512-2VlU59N5P4HaumDK1Z3XEVjSvegFbEOQRgpHUBaB2Ak98Axl3hFhJ6RFcNQNuk9SfL6WxIbuLst8dW/U56NSiA==", + "requires": { + "jquery": ">=1.8.0 <4.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -22379,6 +22479,12 @@ "minimist": "^1.2.6" } }, + "moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index b6910f8cc..6feba492d 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,10 @@ "@types/greasemonkey": "4.0.3", "@types/jest": "28.1.1", "@types/jest-when": "3.5.0", + "@types/jquery": "^3.5.14", + "@types/jqueryui": "^1.12.16", "@types/postcss-preset-env": "6.7.3", + "@types/resemblejs": "^4.1.0", "@types/rollup__plugin-virtual": "2.0.1", "@types/rollup-plugin-progress": "1.1.1", "@types/setup-polly-jest": "0.5.1", @@ -88,6 +91,7 @@ "jest-html-reporters": "3.0.9", "jest-when": "3.5.1", "license-checker": "25.0.1", + "moment": "^2.29.3", "nativejsx": "github:ROpdebee/nativejsx", "node-fetch": "3.2.6", "postcss": "8.4.14", @@ -109,6 +113,7 @@ }, "dependencies": { "idb": "^7.0.1", + "jquery-ui": "^1.13.1", "p-retry": "^5.1.1", "p-throttle": "5.0.0", "ts-custom-error": "3.2.0" diff --git a/src/lib/MB/types-api.ts b/src/lib/MB/types-api.ts index 566f0b851..caa640b4b 100644 --- a/src/lib/MB/types-api.ts +++ b/src/lib/MB/types-api.ts @@ -69,9 +69,9 @@ export interface Place { } export interface RelationshipDate { - day: number | null; - month: number | null; - year: number | null; + day?: number | null; + month?: number | null; + year?: number | null; } interface BaseRecordingRelationship { @@ -115,6 +115,30 @@ export interface ReleaseRecordingRels { }; } +export interface Release { + id: string; + packagingID: number; + statusID: number; + combined_format_name?: string; + barcode?: string; + events?: Array<{ + country?: { primary_code: string }; + date?: RelationshipDate; + }>; + labels?: Array<{ + catalogNumber?: string; + label?: { + gid: string; + name: string; + }; + }>; + mediums: Array<{ + format?: { + name: string; + }; + }>; +} + interface BaseAPIResponse { created: string; count: number; diff --git a/src/lib/MB/types.ts b/src/lib/MB/types.ts index 81e824fe2..7602718fa 100644 --- a/src/lib/MB/types.ts +++ b/src/lib/MB/types.ts @@ -38,6 +38,16 @@ declare global { GIT_BRANCH: string; GIT_SHA: string; }; + $c: { + user: { + preferences: { + datetime_format: string; + }; + }; + stash: { + current_language?: string; + }; + }; }; } } diff --git a/src/lib/util/xhr.ts b/src/lib/util/xhr.ts index 7c0fccb6c..9f90a0575 100644 --- a/src/lib/util/xhr.ts +++ b/src/lib/util/xhr.ts @@ -24,10 +24,10 @@ export class HTTPResponseError extends ResponseError { public readonly statusCode: number; public readonly statusText: string; // eslint-disable-next-line no-restricted-globals - public readonly response: GM.Response; + public readonly response: GM.Response | Response; // eslint-disable-next-line no-restricted-globals - public constructor(url: string | URL, response: GM.Response) { + public constructor(url: string | URL, response: GM.Response | Response) { /* istanbul ignore else: Should not happen */ if (response.statusText.trim()) { super(url, `HTTP error ${response.status}: ${response.statusText}`); diff --git a/src/mb_caa_dimensions/exports.ts b/src/mb_caa_dimensions/exports.ts index 3edd54612..c69560814 100644 --- a/src/mb_caa_dimensions/exports.ts +++ b/src/mb_caa_dimensions/exports.ts @@ -2,33 +2,25 @@ import { logFailure } from '@lib/util/async'; -import type { ImageInfo } from './ImageInfo'; +import type { Dimensions, ImageInfo } from './ImageInfo'; import type { InfoCache } from './InfoCache'; import { CAAImageWithFullSizeURL, displayInfoWhenInView } from './DisplayedImage'; import { CAAImage } from './Image'; -interface LegacyImageInfo { - url: string; - width: number; - height: number; - size?: number; - format?: string; -} +export type ROpdebee_getDimensionsWhenInView = (imgElement: HTMLImageElement) => void; +export type ROpdebee_getCAAImageInfo = (imgUrl: string) => Promise; +export type ROpdebee_getImageDimensions = (imgUrl: string) => Promise; declare global { interface Window { - ROpdebee_getDimensionsWhenInView: (imgElement: HTMLImageElement) => void; - ROpdebee_getCAAImageInfo: (imgUrl: string) => Promise; - ROpdebee_loadImageDimensions: (imgUrl: string) => Promise; + ROpdebee_getDimensionsWhenInView: ROpdebee_getDimensionsWhenInView; + ROpdebee_getCAAImageInfo: ROpdebee_getCAAImageInfo; + ROpdebee_getImageDimensions: ROpdebee_getImageDimensions; } } export function setupExports(cachePromise: Promise): void { async function getCAAImageInfo(imgUrl: string): Promise { - if (new URL(imgUrl).hostname !== 'archive.org') { - throw new Error('Unsupported URL: Need direct image URL'); - } - const cache = await cachePromise; const image = new CAAImage(imgUrl, cache); return image.getImageInfo(); @@ -41,19 +33,14 @@ export function setupExports(cachePromise: Promise): void { }), `Something went wrong when attempting to load image info for ${imgElement.src}`); } - async function loadImageDimensions(imgUrl: string): Promise { - const imageInfo = await getCAAImageInfo(imgUrl); - return { - url: imgUrl, - ...imageInfo.dimensions ?? { width: 0, height: 0 }, - size: imageInfo.size, - format: imageInfo.fileType, - }; + async function getImageDimensions(imgUrl: string): Promise { + const cache = await cachePromise; + const image = new CAAImage(imgUrl, cache); + return image.getDimensions(); } // Expose the function for use in other scripts that may load images. window.ROpdebee_getDimensionsWhenInView = getDimensionsWhenInView; - // Deprecated, use `ROpdebee_getImageInfo` instead. - window.ROpdebee_loadImageDimensions = loadImageDimensions; + window.ROpdebee_getImageDimensions = getImageDimensions; window.ROpdebee_getCAAImageInfo = getCAAImageInfo; } diff --git a/src/mb_enhanced_cover_art_uploads/providers/archive.ts b/src/mb_enhanced_cover_art_uploads/providers/archive.ts index c9af393a9..0db120033 100644 --- a/src/mb_enhanced_cover_art_uploads/providers/archive.ts +++ b/src/mb_enhanced_cover_art_uploads/providers/archive.ts @@ -10,12 +10,20 @@ import { gmxhr } from '@lib/util/xhr'; import type { CoverArt } from './base'; import { CoverArtProvider } from './base'; -interface CAAIndex { +// TODO: This should probably be put in lib. +export interface CAAIndex { images: Array<{ comment: string; types: string[]; id: string | number; // Used to be string in the past, hasn't been applied retroactively yet, see CAA-129 image: string; + thumbnails: { + '250': string; + '500': string; + '1200'?: string; + small: string; + large: string; + }; }>; } diff --git a/src/mb_supercharged_caa_edits/CAAEdit.tsx b/src/mb_supercharged_caa_edits/CAAEdit.tsx new file mode 100644 index 000000000..277ba3f07 --- /dev/null +++ b/src/mb_supercharged_caa_edits/CAAEdit.tsx @@ -0,0 +1,467 @@ +import type Moment from 'moment'; + +import type { RelationshipDate, Release } from '@lib/MB/types-api'; +import type { ROpdebee_getImageDimensions } from '@src/mb_caa_dimensions/exports'; +import type { CAAIndex } from '@src/mb_enhanced_cover_art_uploads/providers/archive'; +import { insertBetween } from '@lib/util/array'; +import { logFailure } from '@lib/util/async'; + +import { fixCaaUrl, getDimensionsWhenInView, openComparisonDialog, selectImage } from './comparisonDialog'; +import { LIKELY_DIGITAL_DIMENSIONS, MB_FORMAT_TRANSLATIONS, NONSQUARE_PACKAGING_COVER_TYPES, NONSQUARE_PACKAGING_TYPES, PACKAGING_TYPES, SHADY_REASONS, STATUSES } from './constants'; + +export type CAAImage = CAAIndex['images'][0]; + +const getImageDimensions = ((): ROpdebee_getImageDimensions => { + const actualFn = window.ROpdebee_getImageDimensions; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!actualFn) { + // Don't warn here, if we can't find this function, we likely won't have + // found the other either + return () => Promise.reject('Script unavailable'); + } + + return actualFn; +})(); + +// FIXME: This is duplicated in another script +function stringifyDate(date: RelationshipDate): string { + const year = date.year ? date.year.toString().padStart(4, '0') : '????'; + const month = date.month ? date.month.toString().padStart(2, '0') : '??'; + const day = date.day ? date.day.toString().padStart(2, '0') : '??'; + return [year, month, day].join('-') + .replace(/(?:-\?{2}){1,2}$/, ''); // Remove -?? or -??-?? suffix. + // If neither year, month, or day is set, will return '????' +} + +function translateMBDateFormatToMoments(dateFormat: string): string { + // eslint-disable-next-line unicorn/no-array-reduce + return Object.entries(MB_FORMAT_TRANSLATIONS).reduce((format, [mbToken, momentsToken]) => { + return format.replace(mbToken, momentsToken); + }, dateFormat); +} + +function processReleaseEvents(events: NonNullable): Array<[string, string[]]> { + const dateToCountries = new Map(); + for (const event of events) { + const country = event.country?.primary_code; + const date = stringifyDate(event.date ?? {}); + + if (!dateToCountries.has(date)) { + dateToCountries.set(date, []); + } + + if (country) { + dateToCountries.get(date)!.push(country); + } + } + + const arr = [...dateToCountries.entries()]; + arr.sort((a, b) => { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }); + + return arr; +} + +export class CAAEdit { + private readonly edit: Element; + private readonly releaseDetails: Release; + public readonly otherImages: CAAImage[]; + public readonly currentImage?: CAAImage; + private readonly warningsUl: HTMLUListElement; + private _selectedIdx = 0; + private readonly warningMsgs: Set; + + private anchor?: HTMLAnchorElement; + private compareButton?: HTMLButtonElement; + private typesSpan?: HTMLSpanElement; + private nextButton?: HTMLButtonElement; + private prevButton?: HTMLButtonElement; + + public constructor(edit: Element, releaseDetails: Release, otherImages: CAAImage[], currentImage?: CAAImage) { + this.edit = edit; + this.releaseDetails = releaseDetails; + this.otherImages = otherImages; + this.currentImage = currentImage; + this.setTypes(); + this.insertReleaseInfo(); + this.insertComparisonImages(); + this.warningsUl = this.insertWarnings(); + this.warningMsgs = new Set(); + this.performSanityChecks(); + } + + private setTypes(): void { + const trs = this.edit.querySelectorAll('table.details > tbody > tr'); + const typesRow = [...trs].find((tr) => tr.querySelector('th')!.textContent === 'Types:'); + if (!typesRow) { + this.insertRow('Types:', (none)); + } else { + const td = typesRow.querySelector('td')!; + const existingTypes = td.textContent!; + const newSpans = existingTypes.split(', ') + .map((type) => {type}); + + if (newSpans.length > 0) { + td.innerHTML = ''; + td.append(...insertBetween(newSpans, ', ')); + } + } + } + + private insertWarnings(): HTMLUListElement { + const warningsList =