Skip to content

Instantly share code, notes, and snippets.

@Kenya-West
Last active May 4, 2024 12:33
Show Gist options
  • Save Kenya-West/5d2e6df1ea13ca2e6ab112b9c166d845 to your computer and use it in GitHub Desktop.
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
// ==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