您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Loads new images from VK and Telegram in InoReader articles
当前为
// ==UserScript== // @name InoReader restore lost images // @namespace http://tampermonkey.net/ // @version 0.0.1 // @description Loads new images from VK and Telegram in InoReader articles // @author Kenya-West // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @match https://*.inoreader.com/feed* // @match https://*.inoreader.com/article* // @match https://*.inoreader.com/folder* // @icon https://inoreader.com/favicon.ico?v=8 // @license MIT // ==/UserScript== // @ts-check (function () { "use strict"; const appConfig = { corsProxy: "https://corsproxy.io/?", }; const appState = { readerPaneExists: false, restoreImagesInListView: false, restoreImagesInArticleView: false, }; // 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 mutation of mutationsList) { if (mutation.type === "childList") { mutation.addedNodes.forEach(function (node) { if (node.nodeType === Node.ELEMENT_NODE) { restoreImagesInArticleList(node); runRestoreImagesInArticleView(node); } }); } } }; function registerCommands() { let enableImageRestoreInListViewCommand; let disableImageRestoreInListViewCommand; let enableImageRestoreInArticleViewCommand; let disableImageRestoreInArticleViewCommand; const restoreImageListView = localStorage.getItem("restoreImageListView") ?? "false"; const restoreImageArticleView = localStorage.getItem( "restoreImageArticleView" ) ?? "true"; if (restoreImageListView === "false") { appState.restoreImagesInListView = false; // @ts-ignore enableImageRestoreInListViewCommand = GM_registerMenuCommand( "Enable image restore in article list", () => { localStorage.setItem("restoreImageListView", "true"); appState.restoreImagesInListView = true; if (enableImageRestoreInListViewCommand) { unregisterAllCommands(); registerCommands(); } } ); } else { appState.restoreImagesInListView = true; // @ts-ignore disableImageRestoreInListViewCommand = GM_registerMenuCommand( "Disable image restore in article list", () => { localStorage.setItem("restoreImageListView", "false"); appState.restoreImagesInListView = false; if (disableImageRestoreInListViewCommand) { unregisterAllCommands(); registerCommands(); } } ); } if (restoreImageArticleView === "false") { appState.restoreImagesInArticleView = false; // @ts-ignore enableImageRestoreInArticleViewCommand = GM_registerMenuCommand( "Enable image restore in article view", () => { localStorage.setItem("restoreImageArticleView", "true"); appState.restoreImagesInArticleView = true; if (enableImageRestoreInArticleViewCommand) { unregisterAllCommands(); registerCommands(); } } ); } else { appState.restoreImagesInArticleView = true; // @ts-ignore disableImageRestoreInArticleViewCommand = GM_registerMenuCommand( "Disable image restore in article view", () => { localStorage.setItem("restoreImageArticleView", "false"); appState.restoreImagesInArticleView = false; if (disableImageRestoreInArticleViewCommand) { unregisterAllCommands(); registerCommands(); } } ); } function unregisterCommand(command) { // @ts-ignore GM_unregisterMenuCommand(command); }; function unregisterAllCommands() { // @ts-ignore GM_unregisterMenuCommand(enableImageRestoreInListViewCommand); // @ts-ignore GM_unregisterMenuCommand(disableImageRestoreInListViewCommand); // @ts-ignore GM_unregisterMenuCommand(enableImageRestoreInArticleViewCommand); // @ts-ignore GM_unregisterMenuCommand(disableImageRestoreInArticleViewCommand); } } /** * * @param {Node} node * @returns {void} */ function restoreImagesInArticleList(node) { const readerPane = document.body.querySelector("#reader_pane"); if (readerPane) { if (!appState.readerPaneExists) { appState.readerPaneExists = 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) { for (let mutation of mutationsList) { if (mutation.type === "childList") { mutation.addedNodes.forEach(function (node) { if (node.nodeType === Node.ELEMENT_NODE) { if (appState.restoreImagesInListView) { setTimeout(() => { start(node); }, 500); } } }); } } }; // Options for the observer (which mutations to observe) const mutationObserverLocalConfig = { attributes: false, childList: true, subtree: false, }; // Create an observer instance linked to the callback function const tmObserverImageRestoreReaderPane = new MutationObserver( callback ); // Start observing the target node for configured mutations tmObserverImageRestoreReaderPane.observe( readerPane, mutationObserverLocalConfig ); } } else { appState.readerPaneExists = false; } /** * * @param {Node} node */ function start(node) { const imageElement = getImageElement(node); if (imageElement) { const telegramPostUrl = getTelegramPostUrl(node); const imageUrl = getImageLink(imageElement); if (imageUrl) { console.log( `Found an image in the article list. Image URL: ${imageUrl}, Telegram post URL: ${telegramPostUrl}` ); testImageLink(imageUrl).then(() => { console.log(`Image loaded. Image URL: ${imageUrl}`); replaceImageSrc(imageElement, telegramPostUrl); console.log(`Replaced the image!`); }); } } } /** * * @param {Node} node * @returns {HTMLDivElement | null} */ function getImageElement(node) { /** * @type {HTMLDivElement} */ // @ts-ignore const nodeElement = node; /** * @type {HTMLDivElement | null} */ const divImageElement = nodeElement.querySelector( "a[href*='t.me'] > div[style*='background-image']" ); return divImageElement ?? null; } /** * * @param {Node} node */ function getTelegramPostUrl(node) { /** * @type {HTMLDivElement} */ // @ts-ignore const nodeElement = node; /** * @type {HTMLAnchorElement | null} */ const ahrefElement = nodeElement.querySelector("a[href*='t.me']"); const telegramPostUrl = ahrefElement?.href ?? ""; // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it try { return ( new URL(telegramPostUrl).origin + new URL(telegramPostUrl).pathname ); } catch (error) { return telegramPostUrl?.split("?")[0]; } } /** * * @param {HTMLDivElement} div */ function getImageLink(div) { const backgroundImageUrl = div?.style.backgroundImage; /** * @type {string | undefined} */ let imageUrl; try { imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1]; } catch (error) { imageUrl = backgroundImageUrl?.slice(5, -2); } if (!imageUrl?.startsWith("http")) { console.error( `The image could not be parsed. Image URL: ${imageUrl}` ); return null; } return imageUrl; } /** * * @param {string} imageUrl * @returns {Promise<void>} */ function testImageLink(imageUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.src = imageUrl; img.onload = function () { reject(); }; img.onerror = function () { resolve(); }; }); } /** * * @param {HTMLDivElement} div * @param {string} telegramPostUrl */ async function replaceImageSrc(div, telegramPostUrl) { const doc = await commonFetchTgPostEmbed(telegramPostUrl); const imgLink = commonGetImgUrlFromTgPost(doc); try { div.style.backgroundImage = `url(${imgLink})`; } catch (error) { console.error( `Error parsing the HTML from the telegram post. Error: ${error}` ); } } } /** * * @param {Node} node * @returns {void} */ function runRestoreImagesInArticleView(node) { if (!appState.restoreImagesInArticleView) { return; } /** * @type {HTMLDivElement} */ // @ts-ignore const nodeElement = node; /** * @type {HTMLDivElement | null} */ const articleRoot = nodeElement?.querySelector( querySelectorPathArticleRoot ); if (articleRoot) { getImageLink(articleRoot); return; } /** * * @param {HTMLDivElement} articleRoot */ function getImageLink(articleRoot) { /** * @type {HTMLAnchorElement[]} */ const ahrefElementArr = Array.from( articleRoot.querySelectorAll("a[href*='t.me']") ); /** * @type {HTMLAnchorElement | null} */ const ahrefElement = ahrefElementArr[1] ?? ahrefElementArr[2] ?? ahrefElementArr[0] ?? null; /** * @type {string | undefined} telegramPostUrl */ let telegramPostUrl = ahrefElement?.href ?? ""; // try to get rid of urlsearchparams. If it fails, get rid of the question mark and everything after it try { telegramPostUrl = new URL(telegramPostUrl).origin + new URL(telegramPostUrl).pathname; } catch (error) { telegramPostUrl = telegramPostUrl?.split("?")[0]; } articleRoot.querySelectorAll("img")?.forEach( /** * * @param {HTMLImageElement} img */ function (img) { const attributes = img.attributes; const originalSrcLink = attributes?.getNamedItem("data-original-src")?.value; if (originalSrcLink?.includes("cdn-telegram.org")) { img.onerror = function () { if (telegramPostUrl) { replaceImageSrc(img, telegramPostUrl); } else { console.error( `The image could not be loaded and no Telegram post found. Telegram post URL: ${telegramPostUrl}. Img original src: ${originalSrcLink}` ); } }; } } ); } /** * * @param {HTMLImageElement} img * @param {string} telegramPostUrl */ async function replaceImageSrc(img, telegramPostUrl) { const doc = await commonFetchTgPostEmbed(telegramPostUrl); const imgLink = commonGetImgUrlFromTgPost(doc); try { img.src = imgLink ?? ""; img.setAttribute("data-original-src", imgLink ?? ""); } catch (error) { console.error( `Error parsing the HTML from the telegram post. Error: ${error}` ); } } } /** * * @param telegramPostUrl string * @returns {Promise<Document>} */ async function commonFetchTgPostEmbed(telegramPostUrl) { // add ?embed=1 to the end of the telegramPostUrl by constructing URL object const telegramPostUrlObject = new URL(telegramPostUrl); telegramPostUrlObject.searchParams.append("embed", "1"); const requestUrl = appConfig.corsProxy ? appConfig.corsProxy + encodeURIComponent(telegramPostUrlObject.toString()) : telegramPostUrlObject; const response = await fetch(requestUrl); try { const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); return Promise.resolve(doc); } catch (error) { console.error( `Error parsing the HTML from the telegram post. Error: ${error}` ); return Promise.reject(error); } } /** * * @param {Document} doc * @returns {string | undefined} imageUrl */ function commonGetImgUrlFromTgPost(doc) { /** * @type {HTMLAnchorElement | null} */ const img = doc.querySelector( "a[href^='https://t.me/'].tgme_widget_message_photo_wrap" ); // get background-image url from the style attribute const backgroundImageUrl = img?.style.backgroundImage; /** * @type {string | undefined} */ let imageUrl; try { imageUrl = backgroundImageUrl?.match(/url\("(.*)"\)/)?.[1]; } catch (error) { imageUrl = backgroundImageUrl?.slice(5, -2); } // any better way? 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); registerCommands(); })();