From 3c43d7faf832998b5828827f018bbc1a062682c8 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 17 Feb 2025 16:45:00 -0800 Subject: [PATCH 1/8] fix disable gifs in blog cards --- src/features/accesskit/disable_gifs.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/features/accesskit/disable_gifs.js b/src/features/accesskit/disable_gifs.js index 002603fc2f..1291d18d52 100644 --- a/src/features/accesskit/disable_gifs.js +++ b/src/features/accesskit/disable_gifs.js @@ -4,6 +4,7 @@ import { dom } from '../../utils/dom.js'; import { buildStyle, postSelector } from '../../utils/interface.js'; const canvasClass = 'xkit-paused-gif-placeholder'; +const hoverContainerAttribute = 'data-paused-gif-hover-container'; const labelClass = 'xkit-paused-gif-label'; const containerClass = 'xkit-paused-gif-container'; const backgroundGifClass = 'xkit-paused-background-gif'; @@ -42,8 +43,8 @@ export const styleElement = buildStyle(` *:hover > .${canvasClass}, *:hover > .${labelClass}, -.${containerClass}:hover .${canvasClass}, -.${containerClass}:hover .${labelClass} { +[${hoverContainerAttribute}]:hover .${canvasClass}, +[${hoverContainerAttribute}]:hover .${labelClass} { display: none; } @@ -57,7 +58,10 @@ export const styleElement = buildStyle(` } `); +const blogCarouselBackgroundSelector = `${keyToCss('background')} > *`; + const addLabel = (element, inside = false) => { + if (element.matches(blogCarouselBackgroundSelector)) return; if (element.parentNode.querySelector(`.${labelClass}`) === null) { const gifLabel = document.createElement('p'); gifLabel.className = element.clientWidth && element.clientWidth < 150 @@ -120,7 +124,7 @@ const processRows = function (rowsElements) { if (row.previousElementSibling?.classList?.contains(containerClass)) { row.previousElementSibling.append(row); } else { - const wrapper = dom('div', { class: containerClass }); + const wrapper = dom('div', { class: containerClass, [hoverContainerAttribute]: '' }); row.replaceWith(wrapper); wrapper.append(row); } @@ -128,6 +132,9 @@ const processRows = function (rowsElements) { }); }; +const processHoverableElements = elements => + elements.forEach(element => element.setAttribute(hoverContainerAttribute, '')); + export const main = async function () { const gifImage = ` :is(figure, ${keyToCss('tagImage', 'takeoverBanner')}) img[srcset*=".gif"]:not(${keyToCss('poster')}) @@ -139,6 +146,11 @@ export const main = async function () { `; pageModifications.register(gifBackgroundImage, processBackgroundGifs); + pageModifications.register( + `${keyToCss('listTimelineObject')} ${keyToCss('carouselWrapper')} ${keyToCss('postCard')}`, + processHoverableElements + ); + pageModifications.register( `:is(${postSelector}, ${keyToCss('blockEditorContainer')}) ${keyToCss('rows')}`, processRows @@ -149,6 +161,7 @@ export const clean = async function () { pageModifications.unregister(processGifs); pageModifications.unregister(processBackgroundGifs); pageModifications.unregister(processRows); + pageModifications.unregister(processHoverableElements); [...document.querySelectorAll(`.${containerClass}`)].forEach(wrapper => wrapper.replaceWith(...wrapper.children) @@ -156,4 +169,5 @@ export const clean = async function () { $(`.${canvasClass}, .${labelClass}`).remove(); $(`.${backgroundGifClass}`).removeClass(backgroundGifClass); + $(`[${hoverContainerAttribute}]`).removeAttr(hoverContainerAttribute); }; From ec85072342e53be423349fd8c67424b6d554c9fa Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 18 Feb 2025 03:38:33 -0800 Subject: [PATCH 2/8] revert label fix --- src/features/accesskit/disable_gifs.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/features/accesskit/disable_gifs.js b/src/features/accesskit/disable_gifs.js index 1291d18d52..fa0133736d 100644 --- a/src/features/accesskit/disable_gifs.js +++ b/src/features/accesskit/disable_gifs.js @@ -58,10 +58,7 @@ export const styleElement = buildStyle(` } `); -const blogCarouselBackgroundSelector = `${keyToCss('background')} > *`; - const addLabel = (element, inside = false) => { - if (element.matches(blogCarouselBackgroundSelector)) return; if (element.parentNode.querySelector(`.${labelClass}`) === null) { const gifLabel = document.createElement('p'); gifLabel.className = element.clientWidth && element.clientWidth < 150 From 9a7ffbfd3340faba6f9163d1b6d77ccb801da222 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 15 Feb 2025 21:59:42 -0800 Subject: [PATCH 3/8] install @types/dom-webcodecs --- package-lock.json | 8 ++++++++ package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index 996c76ff39..9158f7b4c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "webextension-polyfill": "^0.12.0" }, "devDependencies": { + "@types/dom-webcodecs": "^0.1.13", "chrome-webstore-upload-cli": "^3.3.1", "eslint": "^8.57.1", "eslint-config-semistandard": "^17.0.0", @@ -475,6 +476,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", diff --git a/package.json b/package.json index 90c0ab6b76..924c28247f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "web-ext build" }, "devDependencies": { + "@types/dom-webcodecs": "^0.1.13", "chrome-webstore-upload-cli": "^3.3.1", "eslint": "^8.57.1", "eslint-config-semistandard": "^17.0.0", From 2d9d4a4d161e18c8a59dcdde20d23fc1c4d2fd18 Mon Sep 17 00:00:00 2001 From: Marcus Date: Thu, 20 Feb 2025 03:47:09 -0800 Subject: [PATCH 4/8] implement poster and css pausing --- src/features/accesskit/disable_gifs.js | 146 +++++++++++++++---------- 1 file changed, 90 insertions(+), 56 deletions(-) diff --git a/src/features/accesskit/disable_gifs.js b/src/features/accesskit/disable_gifs.js index fa0133736d..bca411f044 100644 --- a/src/features/accesskit/disable_gifs.js +++ b/src/features/accesskit/disable_gifs.js @@ -2,12 +2,16 @@ import { pageModifications } from '../../utils/mutations.js'; import { keyToCss } from '../../utils/css_map.js'; import { dom } from '../../utils/dom.js'; import { buildStyle, postSelector } from '../../utils/interface.js'; +import { memoize } from '../../utils/memoize.js'; -const canvasClass = 'xkit-paused-gif-placeholder'; +const posterAttribute = 'data-paused-gif-placeholder'; +const pausedContentVar = '--xkit-paused-gif-content'; +const pausedBackgroundImageVar = '--xkit-paused-gif-background-image'; const hoverContainerAttribute = 'data-paused-gif-hover-container'; const labelClass = 'xkit-paused-gif-label'; const containerClass = 'xkit-paused-gif-container'; -const backgroundGifClass = 'xkit-paused-background-gif'; + +const hovered = `:is(:hover > *, [${hoverContainerAttribute}]:hover *)`; export const styleElement = buildStyle(` .${labelClass} { @@ -25,90 +29,116 @@ export const styleElement = buildStyle(` font-weight: bold; line-height: 1em; } - .${labelClass}::before { content: "GIF"; } - .${labelClass}.mini { font-size: 0.6rem; } -.${canvasClass} { - position: absolute; - visibility: visible; - - background-color: rgb(var(--white)); -} - -*:hover > .${canvasClass}, -*:hover > .${labelClass}, -[${hoverContainerAttribute}]:hover .${canvasClass}, -[${hoverContainerAttribute}]:hover .${labelClass} { +.${labelClass}${hovered}, +img:is([${posterAttribute}], [style*="${pausedContentVar}"]):not(${hovered}) ~ div > ${keyToCss('knightRiderLoader')} { display: none; } -.${backgroundGifClass}:not(:hover) { - background-image: none !important; - background-color: rgb(var(--secondary-accent)); +[${posterAttribute}]:not(${hovered}) { + visibility: visible !important; +} +img:has(~ [${posterAttribute}]):not(${hovered}) { + visibility: hidden !important; } -.${backgroundGifClass}:not(:hover) > div { - color: rgb(var(--black)); +img[style*="${pausedContentVar}"]:not(${hovered}) { + content: var(${pausedContentVar}); +} +[style*="${pausedBackgroundImageVar}"]:not(${hovered}) { + background-image: var(${pausedBackgroundImageVar}) !important; } `); const addLabel = (element, inside = false) => { - if (element.parentNode.querySelector(`.${labelClass}`) === null) { + const target = inside ? element : element.parentElement; + if (target) { + [...target.querySelectorAll(`.${labelClass}`)].forEach(existingLabel => existingLabel.remove()); + const gifLabel = document.createElement('p'); - gifLabel.className = element.clientWidth && element.clientWidth < 150 + gifLabel.className = target.clientWidth && target.clientWidth < 150 ? `${labelClass} mini` : labelClass; - inside ? element.append(gifLabel) : element.parentNode.append(gifLabel); + target.append(gifLabel); } }; -const pauseGif = function (gifElement) { - const image = new Image(); - image.src = gifElement.currentSrc; - image.onload = () => { - if (gifElement.parentNode && gifElement.parentNode.querySelector(`.${canvasClass}`) === null) { - const canvas = document.createElement('canvas'); - canvas.width = image.naturalWidth; - canvas.height = image.naturalHeight; - canvas.className = gifElement.className; - canvas.classList.add(canvasClass); - canvas.getContext('2d').drawImage(image, 0, 0); - gifElement.parentNode.append(canvas); - addLabel(gifElement); +const createPausedUrl = memoize(async sourceUrl => { + const response = await fetch(sourceUrl, { headers: { Accept: 'image/webp,*/*' } }); + const contentType = response.headers.get('Content-Type'); + const canvas = document.createElement('canvas'); + + /* globals ImageDecoder */ + if (typeof ImageDecoder === 'function' && await ImageDecoder.isTypeSupported(contentType)) { + const decoder = new ImageDecoder({ + type: contentType, + data: response.body, + preferAnimation: true + }); + const { image: videoFrame } = await decoder.decode(); + if (decoder.tracks.selectedTrack.animated === false) { + // source image is not animated; decline to pause it + return undefined; } - }; -}; + canvas.width = videoFrame.displayWidth; + canvas.height = videoFrame.displayHeight; + canvas.getContext('2d').drawImage(videoFrame, 0, 0); + } else { + if (sourceUrl.endsWith('.webp')) { + // source image may not be animated; decline to pause it + return undefined; + } + const imageBitmap = await response.blob().then(blob => window.createImageBitmap(blob)); + canvas.width = imageBitmap.width; + canvas.height = imageBitmap.height; + canvas.getContext('2d').drawImage(imageBitmap, 0, 0); + } + const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 1)); + return URL.createObjectURL(blob); +}); const processGifs = function (gifElements) { - gifElements.forEach(gifElement => { + gifElements.forEach(async gifElement => { if (gifElement.closest('.block-editor-writing-flow')) return; - const pausedGifElements = [ - ...gifElement.parentNode.querySelectorAll(`.${canvasClass}`), - ...gifElement.parentNode.querySelectorAll(`.${labelClass}`) - ]; - if (pausedGifElements.length) { - gifElement.after(...pausedGifElements); - return; - } - if (gifElement.complete && gifElement.currentSrc) { - pauseGif(gifElement); + const posterElement = gifElement.parentElement.querySelector(keyToCss('poster')); + if (posterElement) { + posterElement.setAttribute(posterAttribute, ''); } else { - gifElement.onload = () => pauseGif(gifElement); + const sourceUrl = gifElement.currentSrc || + await new Promise(resolve => gifElement.addEventListener('load', () => resolve(gifElement.currentSrc), { once: true })); + + const pausedUrl = await createPausedUrl(sourceUrl); + if (!pausedUrl) return; + + gifElement.style.setProperty(pausedContentVar, `url(${pausedUrl})`); } + addLabel(gifElement); + gifElement.decoding = 'sync'; }); }; +const sourceUrlRegex = /(?<=url\(["'])[^)]*?\.(?:gif|gifv|webp)(?=["']\))/g; const processBackgroundGifs = function (gifBackgroundElements) { - gifBackgroundElements.forEach(gifBackgroundElement => { - gifBackgroundElement.classList.add(backgroundGifClass); + gifBackgroundElements.forEach(async gifBackgroundElement => { + const sourceValue = gifBackgroundElement.style.backgroundImage; + const sourceUrl = sourceValue.match(sourceUrlRegex)?.[0]; + if (!sourceUrl) return; + + const pausedUrl = await createPausedUrl(sourceUrl); + if (!pausedUrl) return; + + gifBackgroundElement.style.setProperty( + pausedBackgroundImageVar, + sourceValue.replaceAll(sourceUrlRegex, pausedUrl) + ); addLabel(gifBackgroundElement, true); }); }; @@ -134,12 +164,12 @@ const processHoverableElements = elements => export const main = async function () { const gifImage = ` - :is(figure, ${keyToCss('tagImage', 'takeoverBanner')}) img[srcset*=".gif"]:not(${keyToCss('poster')}) + :is(figure, ${keyToCss('tagImage', 'takeoverBanner')}) img:is([srcset*=".gif"], [src*=".gif"], [srcset*=".webp"], [src*=".webp"]):not(${keyToCss('poster')}) `; pageModifications.register(gifImage, processGifs); const gifBackgroundImage = ` - ${keyToCss('communityHeaderImage', 'bannerImage')}[style*=".gif"] + ${keyToCss('communityHeaderImage', 'bannerImage')}:is([style*=".gif"], [style*=".webp"]) `; pageModifications.register(gifBackgroundImage, processBackgroundGifs); @@ -164,7 +194,11 @@ export const clean = async function () { wrapper.replaceWith(...wrapper.children) ); - $(`.${canvasClass}, .${labelClass}`).remove(); - $(`.${backgroundGifClass}`).removeClass(backgroundGifClass); + $(`.${labelClass}`).remove(); + $(`[${posterAttribute}]`).removeAttr(posterAttribute); $(`[${hoverContainerAttribute}]`).removeAttr(hoverContainerAttribute); + [...document.querySelectorAll(`img[style*="${pausedContentVar}"]`)] + .forEach(element => element.style.removeProperty(pausedContentVar)); + [...document.querySelectorAll(`[style*="${pausedBackgroundImageVar}"]`)] + .forEach(element => element.style.removeProperty(pausedBackgroundImageVar)); }; From 4c3542e6fd916c280faaeefe2de66cf518aac6eb Mon Sep 17 00:00:00 2001 From: Marcus Date: Thu, 20 Feb 2025 03:47:36 -0800 Subject: [PATCH 5/8] post card label fix --- src/features/accesskit/disable_gifs.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/features/accesskit/disable_gifs.js b/src/features/accesskit/disable_gifs.js index bca411f044..cf96dc0ee0 100644 --- a/src/features/accesskit/disable_gifs.js +++ b/src/features/accesskit/disable_gifs.js @@ -40,6 +40,10 @@ export const styleElement = buildStyle(` img:is([${posterAttribute}], [style*="${pausedContentVar}"]):not(${hovered}) ~ div > ${keyToCss('knightRiderLoader')} { display: none; } +${keyToCss('background')} > .${labelClass} { + /* prevent double labels in recommended post cards */ + display: none; +} [${posterAttribute}]:not(${hovered}) { visibility: visible !important; From 31290d6949a15a998b6af84c5ce464912e93ff07 Mon Sep 17 00:00:00 2001 From: Marcus Date: Thu, 20 Feb 2025 21:38:07 -0800 Subject: [PATCH 6/8] safari selector fix --- src/features/accesskit/disable_gifs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/accesskit/disable_gifs.js b/src/features/accesskit/disable_gifs.js index cf96dc0ee0..c5add478b0 100644 --- a/src/features/accesskit/disable_gifs.js +++ b/src/features/accesskit/disable_gifs.js @@ -48,7 +48,7 @@ ${keyToCss('background')} > .${labelClass} { [${posterAttribute}]:not(${hovered}) { visibility: visible !important; } -img:has(~ [${posterAttribute}]):not(${hovered}) { +img:has(~ [${posterAttribute}]:not(${hovered})) { visibility: hidden !important; } From 79643076700501663a0bba6088ce6e4f0b35b474 Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 21 Feb 2025 01:14:38 -0800 Subject: [PATCH 7/8] fix sync decoding in firefox --- src/features/accesskit/disable_gifs.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/accesskit/disable_gifs.js b/src/features/accesskit/disable_gifs.js index c5add478b0..22e23a792e 100644 --- a/src/features/accesskit/disable_gifs.js +++ b/src/features/accesskit/disable_gifs.js @@ -105,13 +105,17 @@ const createPausedUrl = memoize(async sourceUrl => { canvas.getContext('2d').drawImage(imageBitmap, 0, 0); } const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 1)); - return URL.createObjectURL(blob); + const url = URL.createObjectURL(blob); + await dom('img', { src: url }).decode(); + return url; }); const processGifs = function (gifElements) { gifElements.forEach(async gifElement => { if (gifElement.closest('.block-editor-writing-flow')) return; + gifElement.decoding = 'sync'; + const posterElement = gifElement.parentElement.querySelector(keyToCss('poster')); if (posterElement) { posterElement.setAttribute(posterAttribute, ''); @@ -125,7 +129,6 @@ const processGifs = function (gifElements) { gifElement.style.setProperty(pausedContentVar, `url(${pausedUrl})`); } addLabel(gifElement); - gifElement.decoding = 'sync'; }); }; From acc6df90f4d0c5e076ea761a0ba1fa20fd216ccc Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 5 Mar 2025 20:18:45 -0500 Subject: [PATCH 8/8] Revert "install @types/dom-webcodecs" This reverts commit 9a7ffbfd3340faba6f9163d1b6d77ccb801da222. --- package-lock.json | 8 -------- package.json | 1 - 2 files changed, 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9158f7b4c2..996c76ff39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "webextension-polyfill": "^0.12.0" }, "devDependencies": { - "@types/dom-webcodecs": "^0.1.13", "chrome-webstore-upload-cli": "^3.3.1", "eslint": "^8.57.1", "eslint-config-semistandard": "^17.0.0", @@ -476,13 +475,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/dom-webcodecs": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", - "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", diff --git a/package.json b/package.json index 924c28247f..90c0ab6b76 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build": "web-ext build" }, "devDependencies": { - "@types/dom-webcodecs": "^0.1.13", "chrome-webstore-upload-cli": "^3.3.1", "eslint": "^8.57.1", "eslint-config-semistandard": "^17.0.0",