Last active
May 4, 2024 12:33
-
-
Save Kenya-West/5d2e6df1ea13ca2e6ab112b9c166d845 to your computer and use it in GitHub Desktop.
InoReader copy cover image - copy cover image of the post you selected in article list view
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name InoReader copy cover image | |
// @namespace http://tampermonkey.net/ | |
// @version 0.0.2 | |
// @description Copy cover image of the post you selected in article list view | |
// @author Kenya-West | |
// @match https://*.inoreader.com/feed* | |
// @match https://*.inoreader.com/article* | |
// @match https://*.inoreader.com/folder* | |
// @match https://*.inoreader.com/starred* | |
// @match https://*.inoreader.com/library* | |
// @match https://*.inoreader.com/dashboard* | |
// @match https://*.inoreader.com/web_pages* | |
// @match https://*.inoreader.com/trending* | |
// @match https://*.inoreader.com/commented* | |
// @match https://*.inoreader.com/recent* | |
// @match https://*.inoreader.com/search* | |
// @match https://*.inoreader.com/channel* | |
// @match https://*.inoreader.com/teams* | |
// @match https://*.inoreader.com/dashboard* | |
// @match https://*.inoreader.com/pocket* | |
// @match https://*.inoreader.com/liked* | |
// @match https://*.inoreader.com/tags* | |
// @icon https://inoreader.com/favicon.ico?v=8 | |
// @license MIT | |
// ==/UserScript== | |
// @ts-check | |
(function () { | |
"use strict"; | |
document.head.insertAdjacentHTML( | |
"beforeend", | |
` | |
<style> | |
.tm_copy_image_button { | |
display: inline-block; | |
cursor: pointer; | |
position: absolute; | |
right: 0.5rem; | |
top: -2rem; | |
background-color: rgba(0, 0, 0, 0.3); | |
color: white; | |
font-family: 'Inoreader-UI-Icons-Font' !important; | |
font-size: 1.5rem; | |
padding: 0.1rem; | |
border-radius: 50%; | |
margin-left: 0.5rem; | |
transition: background-color 0.3s; | |
} | |
.tm_copy_image_button:hover { | |
background-color: rgba(0, 0, 0, 0.7); | |
transition: background-color 0.3s; | |
} | |
.tm_copy_image_button:active { | |
background-color: rgba(0, 0, 0, 0.9); | |
transition: background-color 0.3s; | |
} | |
.tm_copy_image_button::before { | |
content: "\\ea11"; | |
} | |
.tm_copy_image_button__success::before { | |
content: "\\e976"; | |
} | |
</style>` | |
); | |
/** | |
* @typedef {Object} appConfig | |
* @property {Array<{ | |
* prefixUrl: string, | |
* corsType: "direct" | "corsSh" | "corsAnywhere" | "corsFlare", | |
* token?: string, | |
* hidden?: boolean | |
* }>} corsProxies | |
*/ | |
const appConfig = { | |
}; | |
/** | |
* Represents the application state. | |
* @typedef {Object} AppState | |
* @property {boolean} readerPaneMutationObserverLinked - Indicates whether the reader pane mutation observer is linked. | |
* @property {boolean} articleViewOpened - Indicates whether the article view is opened. | |
* @property {Object} copyBadge - Represents the currently playing video. | |
* @property {HTMLDivElement | null} copyBadge.currentVideoElement - The current video element being played. | |
* @property {function} copyBadge.set - Sets the current video element and pauses the previous one. | |
* @property {function} copyBadge.get - Retrieves the current video element. | |
*/ | |
const appState = { | |
readerPaneMutationObserverLinked: false, | |
articleViewOpened: false, | |
copyBadge: { | |
/** | |
* Represents the currently playing video. | |
* @type {HTMLDivElement | null} | |
*/ | |
currentCopyBadgeElement: null, | |
/** | |
* | |
* @param {HTMLDivElement | null} badge | |
*/ | |
set: (badge) => { | |
const previousCopyBadge = appState.copyBadge.currentCopyBadgeElement; | |
if (previousCopyBadge?.isConnected) { | |
appState.copyBadge.currentCopyBadgeElement?.parentElement?.removeChild(previousCopyBadge); | |
} | |
appState.copyBadge.currentCopyBadgeElement = badge; | |
}, | |
/** | |
* | |
* @returns {HTMLDivElement | null} | |
*/ | |
get: () => { | |
return appState.copyBadge.currentCopyBadgeElement; | |
}, | |
disconnect: () => { | |
if (appState.copyBadge.currentCopyBadgeElement?.isConnected) { | |
appState.copyBadge.currentCopyBadgeElement?.parentElement?.removeChild(appState.copyBadge.currentCopyBadgeElement); | |
appState.copyBadge.currentCopyBadgeElement = null; | |
} | |
}, | |
}, | |
}; | |
// Select the node that will be observed for mutations | |
const targetNode = document.body; | |
// Options for the observer (which mutations to observe) | |
const mutationObserverGlobalConfig = { | |
attributes: false, | |
childList: true, | |
subtree: true, | |
}; | |
const querySelectorPathArticleRoot = ".article_full_contents .article_content"; | |
/** | |
* Callback function to execute when mutations are observed | |
* @param {MutationRecord[]} mutationsList - List of mutations observed | |
* @param {MutationObserver} observer - The MutationObserver instance | |
*/ | |
const callback = function (mutationsList, observer) { | |
for (let i = 0; i < mutationsList.length; i++) { | |
if (mutationsList[i].type === "childList") { | |
mutationsList[i].addedNodes.forEach(function (node) { | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
setCopyIconInArticleList(node); | |
} | |
}); | |
} | |
} | |
}; | |
// | |
// | |
// FIRST PART - RESTORE IMAGES IN ARTICLE LIST | |
// | |
// | |
// | |
/** | |
* | |
* @param {Node} node | |
* @returns {void} | |
*/ | |
function setCopyIconInArticleList(node) { | |
/** | |
* @type {MutationObserver | undefined} | |
*/ | |
let tmObserverImageRestoreReaderPane; | |
const readerPane = document.body.querySelector("#reader_pane"); | |
if (readerPane) { | |
if (!appState.readerPaneMutationObserverLinked) { | |
appState.readerPaneMutationObserverLinked = true; | |
/** | |
* Callback function to execute when mutations are observed | |
* @param {MutationRecord[]} mutationsList - List of mutations observed | |
* @param {MutationObserver} observer - The MutationObserver instance | |
*/ | |
const callback = function (mutationsList, observer) { | |
// filter mutations by having id on target and to have only unique id attribute values | |
let filteredMutations = mutationsList | |
// @ts-ignore | |
.filter((mutation) => mutation.target?.id.includes("article_")) | |
// @ts-ignore | |
.filter((mutation, index, self) => self.findIndex((t) => t.target?.id === mutation.target?.id) === index); | |
if (filteredMutations.length === 2) { | |
// check to have only two mutations: one that has .article_current class and one should not | |
const firstMutation = filteredMutations[0]; | |
const secondMutation = filteredMutations[1]; | |
// sort by abscence of .article_current class | |
filteredMutations = [firstMutation, secondMutation].sort((a, b) => { | |
// @ts-ignore | |
return a.target?.classList?.contains("article_current") ? 1 : -1; | |
}); | |
// @ts-ignore | |
if (firstMutation.target?.classList?.contains("article_current") && !secondMutation.target?.classList?.contains("article_current")) { | |
filteredMutations = []; | |
} | |
} | |
for (let mutation of filteredMutations) { | |
if (mutation.type === "attributes") { | |
if (mutation.attributeName === "class") { | |
/** | |
* @type {HTMLDivElement} | |
*/ | |
// @ts-ignore | |
const target = mutation.target; | |
if ( | |
target.classList.contains("article_current") && | |
target.querySelector(".article_tile_content_wraper .article_tile_picture") | |
) { | |
// тут | |
const imageElement = getImageElement(target); | |
if (imageElement) { | |
const imageUrl = getImageLink(imageElement); | |
if (imageUrl) { | |
const button = createButtonElement(imageUrl); | |
placeButton(target, button); | |
appState.copyBadge.set(button); | |
} | |
} | |
} else if ( | |
!target.classList.contains("article_current") && | |
target.querySelector(".article_tile_content_wraper .article_tile_picture") | |
) { | |
// тут если снято выделение | |
} | |
/** | |
* | |
* @param {Node & HTMLDivElement} node | |
* @returns {HTMLDivElement | null} | |
*/ | |
function getImageElement(node) { | |
const nodeElement = node; | |
/** | |
* @type {HTMLDivElement | null} | |
*/ | |
const divImageElement = nodeElement.querySelector("a[href] > div[style*='background-image']"); | |
return divImageElement ?? null; | |
} | |
/** | |
* | |
* @param {HTMLDivElement} div | |
*/ | |
function getImageLink(div) { | |
const backgroundImageUrl = div?.style.backgroundImage; | |
return commonGetUrlFromBackgroundImage(backgroundImageUrl); | |
} | |
/** | |
* | |
* @param {string} imageUrl | |
* @returns {HTMLDivElement} | |
*/ | |
function createButtonElement(imageUrl) { | |
const button = document.createElement("div"); | |
button.className = "tm_copy_image_button"; | |
button.title = "Copy image to clipboard"; | |
button.addEventListener("click", () => { | |
copyImage(imageUrl); | |
}); | |
return button; | |
} | |
/** | |
* | |
* @param {HTMLDivElement} article | |
* @param {HTMLDivElement} buttonElement | |
*/ | |
function placeButton(article, buttonElement) { | |
if (article) { | |
article.appendChild(buttonElement); | |
} else { | |
console.error("Article was not found. Copy button has not been placed"); | |
} | |
} | |
/** | |
* | |
* @param {string} imageLink | |
*/ | |
function copyImage(imageLink) { | |
const img = new Image(); | |
img.crossOrigin = "Anonymous"; // This enables CORS | |
const c = document.createElement("canvas"); | |
const ctx = c.getContext("2d"); | |
/** | |
* @param {string} path | |
* @param {{ (imgBlob: any): void; (arg0: any): void; }} func | |
*/ | |
function setCanvasImage(path, func) { | |
img.onload = function () { | |
// @ts-ignore | |
c.width = this.naturalWidth; | |
// @ts-ignore | |
c.height = this.naturalHeight; | |
// @ts-ignore | |
ctx.drawImage(this, 0, 0); | |
c.toBlob((/** @type {any} */ blob) => { | |
func(blob); | |
}, "image/png"); | |
}; | |
img.src = path; | |
} | |
setCanvasImage(imageLink, (/** @type {any} */ imgBlob) => { | |
navigator.clipboard | |
.write([new ClipboardItem({ "image/png": imgBlob })]) | |
.then((e) => { | |
setSuccessIcon(); | |
}) | |
.catch((e) => { | |
console.error(e); | |
alert( | |
`Failed to copy image to clipboard. This feature may not supported in your browser, or something happened with image. Please try to save it manually. Error: ${ | |
e.message ?? e.body ?? e.toString() ?? e.name ?? e.constructor.name ?? e.constructor.toString() | |
}` | |
); | |
}); | |
}); | |
} | |
function setSuccessIcon() { | |
appState.copyBadge.get()?.classList.add("tm_copy_image_button__success"); | |
} | |
} | |
} | |
} | |
}; | |
// Options for the observer (which mutations to observe) | |
const mutationObserverLocalConfig = { | |
attributes: true, | |
attributeFilter: ["class"], | |
childList: false, | |
subtree: true, | |
}; | |
// Create an observer instance linked to the callback function | |
tmObserverImageRestoreReaderPane = new MutationObserver(callback); | |
// Start observing the target node for configured mutations | |
tmObserverImageRestoreReaderPane.observe(readerPane, mutationObserverLocalConfig); | |
} | |
} else { | |
appState.readerPaneMutationObserverLinked = false; | |
tmObserverImageRestoreReaderPane?.disconnect(); | |
} | |
} | |
/** | |
* | |
* @param {string} backgroundImageUrl | |
* @returns {string | undefined} | |
*/ | |
function commonGetUrlFromBackgroundImage(backgroundImageUrl) { | |
/** | |
* @type {string | undefined} | |
*/ | |
let imageUrl; | |
try { | |
imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1]; | |
} catch (error) { | |
imageUrl = backgroundImageUrl?.slice(5, -2); | |
} | |
if (!imageUrl || imageUrl == "undefined") { | |
return; | |
} | |
if (!imageUrl?.startsWith("http")) { | |
console.error(`The image could not be parsed. Image URL: ${imageUrl}`); | |
return; | |
} | |
return imageUrl; | |
} | |
// Create an observer instance linked to the callback function | |
const tmObserverImageRestore = new MutationObserver(callback); | |
// Start observing the target node for configured mutations | |
tmObserverImageRestore.observe(targetNode, mutationObserverGlobalConfig); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment