1
0
mirror of https://github.com/koloml/philomena-tagging-assistant.git synced 2026-09-22 20:09:39 +00:00

Find and modify duplicate reports, show source url icons in the diff

This commit is contained in:
2026-08-30 05:44:26 +02:00
parent f873cae544
commit d19764699f
8 changed files with 225 additions and 0 deletions

View File

@@ -104,6 +104,17 @@
"css": [
"src/styles/content/tag-presets.scss"
]
},
{
"matches": [
"*://*.furbooru.org/duplicate_reports"
],
"js": [
"src/content/duplicate-reports.ts"
],
"css": [
"src/styles/content/duplicate-reports.scss"
]
}
],
"action": {

View File

@@ -24,4 +24,17 @@ export default class ImageContainer extends BaseComponent {
return JSON.parse(jsonUris);
}
extractSources(): string[] {
const jsonSourceUrls = this.container.dataset.sourceUrls;
let sourceUrls: string[] | null = null;
try {
sourceUrls = JSON.parse(jsonSourceUrls || '[]');
} catch (e) {
console.warn('Failed to parse source URLs for the image!', this, e);
}
return sourceUrls || [];
}
}

View File

@@ -0,0 +1,86 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import { createFontAwesomeIcon } from "$lib/dom-utils";
const brandsSubtype = 'brands';
const blueskyIcon = ['bluesky', brandsSubtype];
const twitterIcon = ['x-twitter', brandsSubtype];
export default class DupeDiff extends BaseComponent {
#sourcesLine: HTMLElement | null = null;
protected init() {
const {
"6": sourcesLine,
} = this.container.querySelectorAll<HTMLElement>('table tr > td')
this.#sourcesLine = sourcesLine;
}
renderSources(leftSources: string[], rightSources: string[]): void {
if (!this.#sourcesLine) {
console.error("Can't render sources since no sources line in diff fonud!");
return;
}
for (const childElement of this.#sourcesLine.children) {
childElement.remove();
}
const sourceIconsContainer = document.createElement('span');
sourceIconsContainer.classList.add('source-icons');
sourceIconsContainer.append(
this.#renderSourceIcons(leftSources),
" vs ",
this.#renderSourceIcons(rightSources),
);
this.#sourcesLine.append(sourceIconsContainer);
}
#renderSourceIcons(sourcesList: string[]): HTMLElement {
const iconsContainer = document.createElement('span');
iconsContainer.classList.add('source-icons__list');
for (const sourceUrl of sourcesList) {
iconsContainer.append(this.#renderIcon(sourceUrl));
}
if (!sourcesList.length) {
iconsContainer.append('(none)');
}
return iconsContainer;
}
#renderIcon(url: string): HTMLElement {
let iconSlug = 'globe';
let maybeSubtype: string | undefined;
for (const [iconOrIconWithSubtype, singleOrMultiPattern] of DupeDiff.#iconsToPatternsMap) {
if (
typeof singleOrMultiPattern === 'string' && url.includes(singleOrMultiPattern)
|| Array.isArray(singleOrMultiPattern) && singleOrMultiPattern.some(singlePattern => url.includes(singlePattern))
) {
if (Array.isArray(iconOrIconWithSubtype)) {
[iconSlug, maybeSubtype] = iconOrIconWithSubtype;
} else {
iconSlug = iconOrIconWithSubtype;
}
break;
}
}
const sourceIcon = createFontAwesomeIcon(iconSlug, maybeSubtype);
sourceIcon.title = url;
return sourceIcon;
}
static #iconsToPatternsMap = new Map<string[] | string, string[] | string>([
[blueskyIcon, 'bsky.app'],
['paw', 'furaffinity.net'],
[twitterIcon, ['x.com', 'twitter.com']],
]);
}

View File

@@ -0,0 +1,19 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import ImageContainer from "$content/components/philomena/ImageContainer";
export default class DupeImage extends BaseComponent {
readonly imageContainer: ImageContainer;
constructor(container: HTMLElement) {
super(container);
const imageContainerElement = container.querySelector<HTMLElement>('.image-container');
if (!imageContainerElement) {
throw new Error('Missing image container inside the dupe row!');
}
this.imageContainer = new ImageContainer(imageContainerElement);
this.imageContainer.initialize();
}
}

View File

@@ -0,0 +1,34 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import DupeDiff from "$content/components/philomena/dupe/DupeDiff";
import DupeImage from "$content/components/philomena/dupe/DupeImage";
export default class DupeReportRow extends BaseComponent {
#leftImage: DupeImage;
#rightImage: DupeImage;
#difference: DupeDiff;
constructor(leftCell: HTMLElement, rightCell: HTMLElement, diffCell: HTMLElement, reportOptions: HTMLElement) {
super(reportOptions);
this.#leftImage = new DupeImage(leftCell);
this.#rightImage = new DupeImage(rightCell);
this.#difference = new DupeDiff(diffCell);
}
protected build() {
this.#leftImage.initialize();
this.#rightImage.initialize();
this.#difference.initialize();
}
protected init() {
this.#extractAndRenderSourcesIcons();
}
#extractAndRenderSourcesIcons() {
this.#difference.renderSources(
this.#leftImage.imageContainer.extractSources(),
this.#rightImage.imageContainer.extractSources(),
);
}
}

View File

@@ -0,0 +1,49 @@
import { BaseComponent } from "$content/components/base/BaseComponent";
import DupeReportRow from "$content/components/philomena/dupe/DupeReportRow";
export default class GridDupeReportsList extends BaseComponent {
readonly #reports: DupeReportRow[] = [];
protected build() {
const childrenElements = this.container.children;
for (let cellIndex = 0; cellIndex < this.container.childElementCount; cellIndex += 4) {
const startingCell = childrenElements.item(cellIndex);
// First 4 cells are actually table headers, skipping them.
if (!(startingCell instanceof HTMLElement) || startingCell.tagName === 'P') {
continue;
}
const rightImageCell = startingCell.nextElementSibling;
const diffCell = rightImageCell?.nextElementSibling || null;
const reportOptionsCell = diffCell?.nextElementSibling || null;
if (!(rightImageCell instanceof HTMLElement) || !(diffCell instanceof HTMLElement) || !(reportOptionsCell instanceof HTMLElement)) {
console.error(`Unable to capture duplicate report row from starting cell at index ${cellIndex}!`);
continue;
}
this.#reports.push(
new DupeReportRow(
startingCell,
rightImageCell,
diffCell,
reportOptionsCell,
)
);
}
}
protected init() {
for (const report of this.#reports) {
report.initialize();
}
}
static findAndInitialize() {
for (const container of document.querySelectorAll<HTMLElement>('.grid--dupe-report-list')) {
new GridDupeReportsList(container).initialize();
}
}
}

View File

@@ -0,0 +1,3 @@
import GridDupeReportsList from "$content/components/philomena/dupe/GridDupeReportsList";
GridDupeReportsList.findAndInitialize();

View File

@@ -0,0 +1,10 @@
.dr__diff {
.source-icons {
margin-left: .5em;
&__list {
display: inline-flex;
gap: .25em;
}
}
}