From c23c871337871d97d952c2b973adc5ae0ff4b52d Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 30 Aug 2026 04:26:13 +0200 Subject: [PATCH 01/10] Extracted image container into separate class for dupes It uses the same element, and it's probably just better to extract this logic into separate class instead of doing work in different classes. --- .../components/philomena/ImageContainer.ts | 27 +++++++++++++++++++ src/content/components/philomena/MediaBox.ts | 23 +++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 src/content/components/philomena/ImageContainer.ts diff --git a/src/content/components/philomena/ImageContainer.ts b/src/content/components/philomena/ImageContainer.ts new file mode 100644 index 0000000..227da52 --- /dev/null +++ b/src/content/components/philomena/ImageContainer.ts @@ -0,0 +1,27 @@ +import { BaseComponent } from "$content/components/base/BaseComponent"; + +export default class ImageContainer extends BaseComponent { + #imageLink: HTMLAnchorElement | null = null; + + protected init() { + this.#imageLink = this.container.querySelector('a'); + } + + extractActualTags(): string[] { + return this.#imageLink?.title.split(' | Tagged: ')[1]?.split(', ') || []; + } + + extractTagsAndAliases(): string[] { + return this.container.dataset.imageTagAliases?.split(', ') || []; + } + + extractImageLinks(): App.ImageURIs { + const jsonUris = this.container?.dataset.uris; + + if (!jsonUris) { + throw new Error('Missing URIs!'); + } + + return JSON.parse(jsonUris); + } +} diff --git a/src/content/components/philomena/MediaBox.ts b/src/content/components/philomena/MediaBox.ts index c7b1cc6..20bd571 100644 --- a/src/content/components/philomena/MediaBox.ts +++ b/src/content/components/philomena/MediaBox.ts @@ -3,15 +3,18 @@ import { getComponent } from "$content/components/base/component-utils"; import { buildTagsAndAliasesMap } from "$lib/philomena/tag-utils"; import { on } from "$content/components/events/comms"; import { EVENT_TAGS_UPDATED } from "$content/components/events/tagging-profile-popup-events"; +import ImageContainer from "$content/components/philomena/ImageContainer"; export class MediaBox extends BaseComponent { - #thumbnailContainer: HTMLElement | null = null; - #imageLinkElement: HTMLAnchorElement | null = null; + #imageContainer: ImageContainer | null = null; #tagsAndAliases: Map | null = null; init() { - this.#thumbnailContainer = this.container.querySelector('.image-container'); - this.#imageLinkElement = this.#thumbnailContainer?.querySelector('a') || null; + const imageContainerElement = this.container.querySelector('.image-container'); + + this.#imageContainer = imageContainerElement instanceof HTMLElement + ? new ImageContainer(imageContainerElement) + : null; on(this, EVENT_TAGS_UPDATED, this.#onTagsUpdatedRefreshTagsAndAliases.bind(this)); } @@ -27,8 +30,8 @@ export class MediaBox extends BaseComponent { } #calculateMediaBoxTags() { - const tagAliases: string[] = this.#thumbnailContainer?.dataset.imageTagAliases?.split(', ') || []; - const actualTags = this.#imageLinkElement?.title.split(' | Tagged: ')[1]?.split(', ') || []; + const tagAliases = this.#imageContainer?.extractTagsAndAliases() || []; + const actualTags = this.#imageContainer?.extractActualTags() || []; return buildTagsAndAliasesMap(tagAliases, actualTags); } @@ -52,13 +55,13 @@ export class MediaBox extends BaseComponent { } get imageLinks(): App.ImageURIs { - const jsonUris = this.#thumbnailContainer?.dataset.uris; + const sourceUrls = this.#imageContainer?.extractImageLinks(); - if (!jsonUris) { - throw new Error('Missing URIs!'); + if (!sourceUrls) { + throw new Error('Missing image container!'); } - return JSON.parse(jsonUris); + return sourceUrls; } /** From f873cae5446ec40e50afa2b708aadb1aa52f7622 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 30 Aug 2026 05:27:20 +0200 Subject: [PATCH 02/10] Support creating favicon items of different types I need that for brand icons. Without specified subtype, will point to regular solid icons. --- src/lib/dom-utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/dom-utils.ts b/src/lib/dom-utils.ts index bfef56b..9b2164f 100644 --- a/src/lib/dom-utils.ts +++ b/src/lib/dom-utils.ts @@ -2,10 +2,11 @@ * Reusable function to create icons from FontAwesome. Usable only for website, since extension doesn't host its own * copy of FA styles. Extension should use imports of SVGs inside CSS instead. * @param iconSlug Slug of the icon to be added. + * @param [subtype="solid"] Subtype of the icon. Some icons only exist in specific subtypes. * @return Element with classes for FontAwesome icon added. */ -export function createFontAwesomeIcon(iconSlug: string): HTMLElement { +export function createFontAwesomeIcon(iconSlug: string, subtype: string = 'solid'): HTMLElement { const iconElement = document.createElement('i'); - iconElement.classList.add('fa-solid', `fa-${iconSlug}`); + iconElement.classList.add(`fa-${subtype}`, `fa-${iconSlug}`); return iconElement; } From d19764699f036d81cf7f8bdfb42177c428f5e63b Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 30 Aug 2026 05:44:26 +0200 Subject: [PATCH 03/10] Find and modify duplicate reports, show source url icons in the diff --- manifest.json | 11 +++ .../components/philomena/ImageContainer.ts | 13 +++ .../components/philomena/dupe/DupeDiff.ts | 86 +++++++++++++++++++ .../components/philomena/dupe/DupeImage.ts | 19 ++++ .../philomena/dupe/DupeReportRow.ts | 34 ++++++++ .../philomena/dupe/GridDupeReportsList.ts | 49 +++++++++++ src/content/duplicate-reports.ts | 3 + src/styles/content/duplicate-reports.scss | 10 +++ 8 files changed, 225 insertions(+) create mode 100644 src/content/components/philomena/dupe/DupeDiff.ts create mode 100644 src/content/components/philomena/dupe/DupeImage.ts create mode 100644 src/content/components/philomena/dupe/DupeReportRow.ts create mode 100644 src/content/components/philomena/dupe/GridDupeReportsList.ts create mode 100644 src/content/duplicate-reports.ts create mode 100644 src/styles/content/duplicate-reports.scss diff --git a/manifest.json b/manifest.json index ee12cf4..931fa30 100644 --- a/manifest.json +++ b/manifest.json @@ -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": { diff --git a/src/content/components/philomena/ImageContainer.ts b/src/content/components/philomena/ImageContainer.ts index 227da52..10430a5 100644 --- a/src/content/components/philomena/ImageContainer.ts +++ b/src/content/components/philomena/ImageContainer.ts @@ -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 || []; + } } diff --git a/src/content/components/philomena/dupe/DupeDiff.ts b/src/content/components/philomena/dupe/DupeDiff.ts new file mode 100644 index 0000000..636411b --- /dev/null +++ b/src/content/components/philomena/dupe/DupeDiff.ts @@ -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('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([ + [blueskyIcon, 'bsky.app'], + ['paw', 'furaffinity.net'], + [twitterIcon, ['x.com', 'twitter.com']], + ]); +} diff --git a/src/content/components/philomena/dupe/DupeImage.ts b/src/content/components/philomena/dupe/DupeImage.ts new file mode 100644 index 0000000..b6f93d7 --- /dev/null +++ b/src/content/components/philomena/dupe/DupeImage.ts @@ -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('.image-container'); + + if (!imageContainerElement) { + throw new Error('Missing image container inside the dupe row!'); + } + + this.imageContainer = new ImageContainer(imageContainerElement); + this.imageContainer.initialize(); + } +} diff --git a/src/content/components/philomena/dupe/DupeReportRow.ts b/src/content/components/philomena/dupe/DupeReportRow.ts new file mode 100644 index 0000000..1fcdbf7 --- /dev/null +++ b/src/content/components/philomena/dupe/DupeReportRow.ts @@ -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(), + ); + } +} diff --git a/src/content/components/philomena/dupe/GridDupeReportsList.ts b/src/content/components/philomena/dupe/GridDupeReportsList.ts new file mode 100644 index 0000000..1e8db67 --- /dev/null +++ b/src/content/components/philomena/dupe/GridDupeReportsList.ts @@ -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('.grid--dupe-report-list')) { + new GridDupeReportsList(container).initialize(); + } + } +} diff --git a/src/content/duplicate-reports.ts b/src/content/duplicate-reports.ts new file mode 100644 index 0000000..866a71b --- /dev/null +++ b/src/content/duplicate-reports.ts @@ -0,0 +1,3 @@ +import GridDupeReportsList from "$content/components/philomena/dupe/GridDupeReportsList"; + +GridDupeReportsList.findAndInitialize(); diff --git a/src/styles/content/duplicate-reports.scss b/src/styles/content/duplicate-reports.scss new file mode 100644 index 0000000..7cb33e5 --- /dev/null +++ b/src/styles/content/duplicate-reports.scss @@ -0,0 +1,10 @@ +.dr__diff { + .source-icons { + margin-left: .5em; + + &__list { + display: inline-flex; + gap: .25em; + } + } +} From b87a8a43e08eea92e10d760fef5be4e06e7a036e Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 30 Aug 2026 06:23:58 +0200 Subject: [PATCH 04/10] Display ratings when duplicate reports have different ratings --- src/config/tags.ts | 12 ++++++++++ .../components/philomena/dupe/DupeDiff.ts | 23 +++++++++++++++++++ .../philomena/dupe/DupeReportRow.ts | 13 +++++++++++ src/styles/content/duplicate-reports.scss | 10 +++++++- 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/config/tags.ts b/src/config/tags.ts index 2065e88..98583ad 100644 --- a/src/config/tags.ts +++ b/src/config/tags.ts @@ -128,3 +128,15 @@ export const tagsBlacklist: string[] = (__CURRENT_SITE__ === 'furbooru' ? [ "solo oc", "tag your shit" ]); + +/** + * Core rating tags used in the Philomena. These rarely change, so they're pretty safe to hardcode into the source code. + */ +export const ratingTags: string[] = [ + 'safe', + 'suggestive', + 'questionable', + 'explicit', + 'grimdark', + 'grotesque', +]; diff --git a/src/content/components/philomena/dupe/DupeDiff.ts b/src/content/components/philomena/dupe/DupeDiff.ts index 636411b..d2a397e 100644 --- a/src/content/components/philomena/dupe/DupeDiff.ts +++ b/src/content/components/philomena/dupe/DupeDiff.ts @@ -8,13 +8,16 @@ const twitterIcon = ['x-twitter', brandsSubtype]; export default class DupeDiff extends BaseComponent { #sourcesLine: HTMLElement | null = null; + #ratingsLine: HTMLElement | null = null; protected init() { const { "6": sourcesLine, + "7": ratingsLine, } = this.container.querySelectorAll('table tr > td') this.#sourcesLine = sourcesLine; + this.#ratingsLine = ratingsLine; } renderSources(leftSources: string[], rightSources: string[]): void { @@ -39,6 +42,26 @@ export default class DupeDiff extends BaseComponent { this.#sourcesLine.append(sourceIconsContainer); } + renderRatings(leftRating: string | null, rightRating: string | null) { + if (!this.#ratingsLine) { + return; + } + + for (const childElement of this.#ratingsLine.children) { + childElement.remove(); + } + + if (leftRating === rightRating) { + return; + } + + const ratingDifference = document.createElement('span'); + ratingDifference.classList.add('ratings-difference'); + ratingDifference.textContent = `(${leftRating || '(none)'} vs ${rightRating || '(none)'})`; + + this.#ratingsLine.append(ratingDifference); + } + #renderSourceIcons(sourcesList: string[]): HTMLElement { const iconsContainer = document.createElement('span'); iconsContainer.classList.add('source-icons__list'); diff --git a/src/content/components/philomena/dupe/DupeReportRow.ts b/src/content/components/philomena/dupe/DupeReportRow.ts index 1fcdbf7..5b9ab9f 100644 --- a/src/content/components/philomena/dupe/DupeReportRow.ts +++ b/src/content/components/philomena/dupe/DupeReportRow.ts @@ -1,6 +1,7 @@ import { BaseComponent } from "$content/components/base/BaseComponent"; import DupeDiff from "$content/components/philomena/dupe/DupeDiff"; import DupeImage from "$content/components/philomena/dupe/DupeImage"; +import { ratingTags } from "$config/tags"; export default class DupeReportRow extends BaseComponent { #leftImage: DupeImage; @@ -23,6 +24,7 @@ export default class DupeReportRow extends BaseComponent { protected init() { this.#extractAndRenderSourcesIcons(); + this.#extractAndDisplayRatings(); } #extractAndRenderSourcesIcons() { @@ -31,4 +33,15 @@ export default class DupeReportRow extends BaseComponent { this.#rightImage.imageContainer.extractSources(), ); } + + #extractAndDisplayRatings() { + this.#difference.renderRatings( + DupeReportRow.#extractRating(this.#leftImage), + DupeReportRow.#extractRating(this.#rightImage), + ); + } + + static #extractRating(image: DupeImage): string | null { + return image.imageContainer.extractActualTags().find(tagName => ratingTags.includes(tagName)) || null; + } } diff --git a/src/styles/content/duplicate-reports.scss b/src/styles/content/duplicate-reports.scss index 7cb33e5..99c0fa4 100644 --- a/src/styles/content/duplicate-reports.scss +++ b/src/styles/content/duplicate-reports.scss @@ -1,10 +1,18 @@ +%with-margin { + margin-left: .5em; +} + .dr__diff { .source-icons { - margin-left: .5em; + @extend %with-margin; &__list { display: inline-flex; gap: .25em; } } + + .ratings-difference { + @extend %with-margin; + } } From e1b56586beadce812442fd11f51f0535c16bd653 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 30 Aug 2026 06:30:32 +0200 Subject: [PATCH 05/10] Work with dupe reports list with different arguments --- manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 931fa30..a55ca6d 100644 --- a/manifest.json +++ b/manifest.json @@ -107,7 +107,8 @@ }, { "matches": [ - "*://*.furbooru.org/duplicate_reports" + "*://*.furbooru.org/duplicate_reports", + "*://*.furbooru.org/duplicate_reports?*" ], "js": [ "src/content/duplicate-reports.ts" From f4e92e313a1d0e4a7f21d3977ff653920aab1052 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 30 Aug 2026 07:13:21 +0200 Subject: [PATCH 06/10] Filling mapping of FA icons to different domains Partially ported this list from the Philomena source code. --- .../components/philomena/dupe/DupeDiff.ts | 135 +++++++++++++++++- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/src/content/components/philomena/dupe/DupeDiff.ts b/src/content/components/philomena/dupe/DupeDiff.ts index d2a397e..c4f73d9 100644 --- a/src/content/components/philomena/dupe/DupeDiff.ts +++ b/src/content/components/philomena/dupe/DupeDiff.ts @@ -3,9 +3,6 @@ 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; #ratingsLine: HTMLElement | null = null; @@ -101,9 +98,135 @@ export default class DupeDiff extends BaseComponent { return sourceIcon; } + /** + * Mirroring of mapping from source URL domains to appropriate FontAwesome icons which is used in Philomena. Doesn't + * match 1-to-1, but pretty close. + * + * Keys are the icons and values are the patterns extension should check for each URL. + */ static #iconsToPatternsMap = new Map([ - [blueskyIcon, 'bsky.app'], - ['paw', 'furaffinity.net'], - [twitterIcon, ['x.com', 'twitter.com']], + [ + ['artstation', brandsSubtype], + 'artstation.com' + ], + [ + 'bed', + 'pillowfort.social', + ], + [ + 'bolt-lightning', + 'boosty.to', + ], + [ + ['bluesky', brandsSubtype], + 'bsky.app', + ], + [ + 'brush', + ['artfight.net', 'newgrounds.com'], + ], + [ + 'coffee', + ['ko-fi.com', 'buymeacoffee.com'], + ], + [ + ['deviantart', brandsSubtype], + ['deviantart.com', 'sta.sh', 'fav.me'], + ], + [ + ['discord', brandsSubtype], + ['discordapp.com', 'discord.com', 'discord.gg'], + ], + [ + 'dove', + 'itaku.ee', + ], + [ + ['etsy', brandsSubtype], + 'etsy.com', + ], + [ + ['facebook', brandsSubtype], + ['facebook.com', 'fb.me'], + ], + [ + ['flickr', brandsSubtype], + 'flickr.com', + ], + [ + ['instagram', brandsSubtype], + 'instagram.com', + ], + [ + ['mastodon', brandsSubtype], + [ + 'awoo.space', + 'bark.light', + 'equestria.social', + 'mastodon.social', + 'meow.social', + 'pawoo.net', + 'pettingzoo.co', + 'pony.social', + 'vulpine.club', + 'yiff.life', + 'socel.net', + 'octodon.social', + 'filly.social', + 'pone.social', + 'hooves.social', + 'baraag.net', + 'furries.club', + ], + ], + [ + 'palette', + ['ych.art', 'commishes.com'] + ], + [ + ['patreon', brandsSubtype], + 'patreon.com' + ], + [ + 'paw', + ['furaffinity.net', 'e621.net', 'furbooru.org', 'inkbunny.net', 'e926.net', 'sofurry.com', 'weasyl.co'], + ], + [ + ['pixiv', brandsSubtype], + ['pixiv.net', 'pixiv.me'], + ], + [ + ['reddit', brandsSubtype], + ['reddit.com', 'redd.it'], + ], + // Patreon piracy website, applying custom icon to easily catch such posts. + [ + 'skull-crossbones', + 'kemono.cr', + ], + [ + ['telegram', brandsSubtype], + 't.me', + ], + [ + ['tiktok', brandsSubtype], + 'tiktok.com' + ], + [ + ['tumblr', brandsSubtype], + ['tumblr.com', 'tmblr.co', 'tumbex.com'], + ], + [ + ['vk', brandsSubtype], + ['vk.com', 'vk.ru'], + ], + [ + ['x-twitter', brandsSubtype], + ['x.com', 'twitter.com', 'twimg.com'] + ], + [ + ['youtube', brandsSubtype], + ['youtube.com', 'youtu.be'], + ], ]); } From b1c85efe0397db181589449688de238f80eba4ec Mon Sep 17 00:00:00 2001 From: KoloMl Date: Thu, 1 Oct 2026 07:23:20 +0200 Subject: [PATCH 07/10] Support updated duplicate reports table from latest Philomena update --- .../philomena/dupe/GridDupeReportsList.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/content/components/philomena/dupe/GridDupeReportsList.ts b/src/content/components/philomena/dupe/GridDupeReportsList.ts index 1e8db67..4ab8d71 100644 --- a/src/content/components/philomena/dupe/GridDupeReportsList.ts +++ b/src/content/components/philomena/dupe/GridDupeReportsList.ts @@ -5,22 +5,16 @@ export default class GridDupeReportsList extends BaseComponent { readonly #reports: DupeReportRow[] = []; protected build() { - const childrenElements = this.container.children; + const repotRowsList = this.container.querySelectorAll('tbody > tr'); - 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; + for (const reportRow of repotRowsList) { + const startingCell = reportRow.firstElementChild; + const rightImageCell = startingCell?.nextElementSibling || null; 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}!`); + if (!(startingCell instanceof HTMLElement) || !(rightImageCell instanceof HTMLElement) || !(diffCell instanceof HTMLElement) || !(reportOptionsCell instanceof HTMLElement)) { + console.error(`Unable to capture duplicate report!`, reportRow); continue; } @@ -42,7 +36,7 @@ export default class GridDupeReportsList extends BaseComponent { } static findAndInitialize() { - for (const container of document.querySelectorAll('.grid--dupe-report-list')) { + for (const container of document.querySelectorAll('.dupe-report-list')) { new GridDupeReportsList(container).initialize(); } } From bdac2692cef1dd4bfc7385468e90196a90348a2a Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 14 Oct 2026 01:25:51 +0200 Subject: [PATCH 08/10] Fixed dupe reports rows detection accidentally trying to use diff rows --- src/content/components/philomena/dupe/GridDupeReportsList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/components/philomena/dupe/GridDupeReportsList.ts b/src/content/components/philomena/dupe/GridDupeReportsList.ts index 4ab8d71..902f1d2 100644 --- a/src/content/components/philomena/dupe/GridDupeReportsList.ts +++ b/src/content/components/philomena/dupe/GridDupeReportsList.ts @@ -5,7 +5,7 @@ export default class GridDupeReportsList extends BaseComponent { readonly #reports: DupeReportRow[] = []; protected build() { - const repotRowsList = this.container.querySelectorAll('tbody > tr'); + const repotRowsList = this.container.querySelectorAll(':scope > tbody > tr'); for (const reportRow of repotRowsList) { const startingCell = reportRow.firstElementChild; From d75a9b434d8ece17ac630739eccc80140ece5c25 Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 14 Oct 2026 01:26:37 +0200 Subject: [PATCH 09/10] More mobile-friendly dupe reports view --- src/styles/content/duplicate-reports.scss | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/styles/content/duplicate-reports.scss b/src/styles/content/duplicate-reports.scss index 99c0fa4..2cff2e4 100644 --- a/src/styles/content/duplicate-reports.scss +++ b/src/styles/content/duplicate-reports.scss @@ -16,3 +16,35 @@ @extend %with-margin; } } + +// Trying to make it more usable on mobile by regrouping table cells into grid. +@media screen and (max-width: 800px) { + .dupe-report-list { + & > thead { + display: none; + } + + & > tbody > tr { + display: grid; + grid-template-areas: "source target" "diff actions"; + grid-template-columns: 1fr 1fr; + + .dupe-report-list--image { + grid-area: source; + width: auto; + + + .dupe-report-list--image { + grid-area: target; + } + } + + .dr__diff { + grid-area: diff; + + & + td { + grid-area: actions; + } + } + } + } +} From 665057d9f3d3abc7d4067fcb932615ec95a8e1df Mon Sep 17 00:00:00 2001 From: KoloMl Date: Wed, 14 Oct 2026 01:26:58 +0200 Subject: [PATCH 10/10] Fixed some diff rows with excessive amount of sources breaking layout --- src/styles/content/duplicate-reports.scss | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/styles/content/duplicate-reports.scss b/src/styles/content/duplicate-reports.scss index 2cff2e4..e5f2a28 100644 --- a/src/styles/content/duplicate-reports.scss +++ b/src/styles/content/duplicate-reports.scss @@ -1,19 +1,15 @@ -%with-margin { - margin-left: .5em; -} - .dr__diff { .source-icons { - @extend %with-margin; - &__list { display: inline-flex; gap: .25em; } } - .ratings-difference { - @extend %with-margin; + td { + display: flex; + flex-wrap: wrap; + column-gap: .5em; } }