// ==UserScript==
// @name InoReader get Habr stats
// @namespace http://tampermonkey.net/
// @version 0.0.1
// @description Shows comments and rating counter for Habr 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*
// @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";
/**
* @typedef {Object} appConfig
* @property {Array<{
* prefixUrl: string,
* corsType: "direct" | "corsSh" | "corsAnywhere" | "corsFlare",
* token?: string,
* hidden?: boolean
* }>} corsProxies
*/
const appConfig = {
corsProxies: [
{
prefixUrl: "https://corsproxy.io/?",
corsType: "direct",
},
{
prefixUrl: "https://proxy.cors.sh/",
corsType: "corsSh",
token: undefined,
hidden: true,
},
{
prefixUrl: "https://cors-anywhere.herokuapp.com/",
corsType: "corsAnywhere",
hidden: true,
},
{
prefixUrl: "https://cors-1.kenyawest.workers.dev/?upstream_url=",
corsType: "corsFlare",
},
],
};
const appState = {
readerPaneArticleListMutationObserverLinked: false,
readerPaneArticleViewMutationObserverLinked: 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";
const LOCAL_STORAGE_LISTVIEW_PARAMNAME="getHabrStatsListView";
const LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME="getHabrStatsArticleView";
/**
* 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) {
if (appState.restoreImagesInListView) {
getHabrStatsInArticleList(node);
}
getHabrStatsInArticleView(node);
}
});
}
}
};
function registerCommands() {
let enableImageRestoreInListViewCommand;
let disableImageRestoreInListViewCommand;
let enableImageRestoreInArticleViewCommand;
let disableImageRestoreInArticleViewCommand;
const restoreImageListView = localStorage.getItem(LOCAL_STORAGE_LISTVIEW_PARAMNAME) ?? "false";
const restoreImageArticleView = localStorage.getItem(LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME) ?? "true";
if (restoreImageListView === "false") {
appState.restoreImagesInListView = false;
// @ts-ignore
enableImageRestoreInListViewCommand = GM_registerMenuCommand("Enable getting Habr stats in article list", () => {
localStorage.setItem(LOCAL_STORAGE_LISTVIEW_PARAMNAME, "true");
appState.restoreImagesInListView = true;
if (enableImageRestoreInListViewCommand) {
unregisterAllCommands();
registerCommands();
}
});
} else {
appState.restoreImagesInListView = true;
// @ts-ignore
disableImageRestoreInListViewCommand = GM_registerMenuCommand("Disable getting Habr stats in article list", () => {
localStorage.setItem(LOCAL_STORAGE_LISTVIEW_PARAMNAME, "false");
appState.restoreImagesInListView = false;
if (disableImageRestoreInListViewCommand) {
unregisterAllCommands();
registerCommands();
}
});
}
if (restoreImageArticleView === "false") {
appState.restoreImagesInArticleView = false;
// @ts-ignore
enableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Enable getting Habr stats in article view", () => {
localStorage.setItem(LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME, "true");
appState.restoreImagesInArticleView = true;
if (enableImageRestoreInArticleViewCommand) {
unregisterAllCommands();
registerCommands();
}
});
} else {
appState.restoreImagesInArticleView = true;
// @ts-ignore
disableImageRestoreInArticleViewCommand = GM_registerMenuCommand("Disable getting Habr stats in article view", () => {
localStorage.setItem(LOCAL_STORAGE_ARTICLEVIEW_PARAMNAME, "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);
}
}
//
//
// FIRST PART - RESTORE IMAGES IN ARTICLE LIST
//
//
//
/**
*
* @param {Node} node
* @returns {void}
*/
function getHabrStatsInArticleList(node) {
/**
* @type {MutationObserver | undefined}
*/
let tmObserverHabrStatsReaderPane;
const readerPane = document.body.querySelector("#reader_pane");
if (readerPane) {
if (!appState.readerPaneArticleListMutationObserverLinked) {
appState.readerPaneArticleListMutationObserverLinked = 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) {
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
tmObserverHabrStatsReaderPane = new MutationObserver(callback);
// Start observing the target node for configured mutations
tmObserverHabrStatsReaderPane.observe(readerPane, mutationObserverLocalConfig);
}
} else {
appState.readerPaneArticleListMutationObserverLinked = false;
tmObserverHabrStatsReaderPane?.disconnect();
}
/**
*
* @param {Node} node
*/
function start(node) {
/**
* @type {Node & HTMLDivElement}
*/
// @ts-ignore
const element = node;
if (element.hasChildNodes() && element.id.includes("article_") && element.classList.contains("ar")) {
const habrLink = getHabrLink(element);
if (habrLink) {
const habrArticle = commonFetchHabrArticle(habrLink);
habrArticle.then(async (habrArticle) => {
const habrStats = commonGetHabrStats(habrArticle);
placeHabrStatsElement(element, habrStats);
});
}
}
}
/**
*
* @param {Node & HTMLDivElement} node
* @returns {string | null}
*/
function getHabrLink(node) {
const nodeElement = node;
/**
* @type {HTMLAnchorElement | null}
*/
const habrLinkElement = nodeElement.querySelector(".article_tile_content_wraper > a[href*='habr.com']");
return habrLinkElement?.href ?? null;
}
/**
*
* @param {HTMLDivElement} div
* @param {{
* rating: number | string,
* comments: number | string,
* }} habrStats
* @returns {void}
*/
function placeHabrStatsElement(div, habrStats) {
const html = `<div class="">
<span class="">
<center>${typeof(habrStats.rating) === 'number' ? habrStats.rating > 0 ? "+" : "-" : ""}${habrStats.rating} | comments: ${habrStats.comments}</center>
</span></div>`;
div.insertAdjacentHTML("beforeend", html);
}
}
//
//
// SECOND PART - RESTORE IMAGES IN ARTICLE VIEW
//
//
//
/**
*
* @param {Node} node
* @returns {void}
*/
function getHabrStatsInArticleView(node) {
if (!appState.restoreImagesInArticleView && appState.readerPaneArticleViewMutationObserverLinked) {
return;
}
/**
* @type {HTMLDivElement}
*/
// @ts-ignore
const nodeElement = node;
/**
* @type {HTMLDivElement | null}
*/
let articleRoot = nodeElement?.querySelector(querySelectorPathArticleRoot);
if (articleRoot === null && nodeElement?.classList.contains("article_full_contents")) {
articleRoot = nodeElement;
}
if (articleRoot) {
appState.readerPaneArticleViewMutationObserverLinked = true;
const habrLink = getHabrLink();
if (habrLink) {
const habrArticle = commonFetchHabrArticle(habrLink);
habrArticle.then(async (habrArticle) => {
const habrStats = commonGetHabrStats(habrArticle);
placeHabrStatsElement(habrStats);
appState.readerPaneArticleViewMutationObserverLinked = false;
});
} else {
appState.readerPaneArticleViewMutationObserverLinked = false;
}
return;
} else {
appState.readerPaneArticleViewMutationObserverLinked = false;
}
/**
*
* @returns {string | null}
*/
function getHabrLink() {
/**
* @type {HTMLDivElement}
*/
// @ts-ignore
const nodeElement = articleRoot;
/**
* @type {HTMLAnchorElement | null}
*/
const habrLinkElement = nodeElement.querySelector(".article_title.article_title_expanded_view > a.article_title_link[href*='habr.com']");
return habrLinkElement?.href ?? null;
}
/**
*
* @param {{
* rating: number | string,
* comments: number | string,
* }} habrStats
* @returns {void}
*/
function placeHabrStatsElement(habrStats) {
/**
* @type {HTMLDivElement}
*/
// @ts-ignore
const nodeElement = articleRoot?.querySelector(".article_footer_placeholder_middle");
const html = `
<div class="article_sub_title graylink_darker">
<center>
${typeof habrStats.rating === "number" ? (habrStats.rating > 0 ? "+" : "-") : ""}${habrStats.rating} | comments: ${habrStats.comments}
</center>
</div>
`;
nodeElement.insertAdjacentHTML("beforebegin", html);
}
}
/**
*
* @param {string} habrLink
* @returns {Promise<Document>}
*/
async function commonFetchHabrArticle(habrLink) {
// add ?embed=1 to the end of the telegramPostUrl by constructing URL object
const habrArticleUrlObject = new URL(habrLink);
const requestUrl = appConfig.corsProxies[3].prefixUrl ? appConfig.corsProxies[3].prefixUrl + habrArticleUrlObject.toString() : habrArticleUrlObject;
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 {{
* rating: number | string,
* comments: number | string,
* }} imageUrl
*/
function commonGetHabrStats(doc) {
/**
* @type {HTMLSpanElement | null}
*/
const ratingElement = doc.querySelector(".tm-article-presenter__body .tm-votes-lever__score-counter[data-test-id='votes-score-counter']");
const rating = Number(ratingElement?.innerText) ?? "N/A";
/**
* @type {HTMLSpanElement | null}
*/
const commentsElement = doc.querySelector(".tm-article-comments-counter-link__value");
const comments = Number(commentsElement?.innerText) ?? "N/A";
const habrStats = {
rating,
comments,
};
return habrStats;
}
// Create an observer instance linked to the callback function
const tmObserverHabrStats = new MutationObserver(callback);
// Start observing the target node for configured mutations
tmObserverHabrStats.observe(targetNode, mutationObserverGlobalConfig);
registerCommands();
})();