您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a button to Feedly that marks items above the current item "as read".
// ==UserScript== // @author Michael Mangino // @name Feedly - Mark Previous As Read // @namespace http://mangino.net/greasemonkey // @description Adds a button to Feedly that marks items above the current item "as read". // @include http://feedly.com/* // @include https://feedly.com/* // @grant GM_info // @icon https://imageshack.us/a/img59/2623/feedlymarkpreviousasrea.png // @version 1.19 // ==/UserScript== //########################################################################### // C O N F I G U R A T I O N //########################################################################### var COLLAPSE_SELECTED_ITEM = false; var IS_DEBUG_ON = false; //########################################################################### // C O N S T A N T S //########################################################################### var FEEDLY_APP_VER_MIN = "16.0.530"; var FEEDLY_APP_VER_MAX = "9999.9999.9999"; var EXCLUDED_PAGES = [ " Today \n ", "Weekend Edition \n ", "\nIndex \n", " Organize \n", " Saved \n", " History \n \n", "Preferences", " \nThemes \n" ]; var COLOR_FEEDLY_GREEN = "#2BB24C"; var BUTTON_MPAR_BG_COLOR_ENABLED = COLOR_FEEDLY_GREEN; var BUTTON_MPAR_BG_COLOR_DISABLED = "darkred"; var BUTTON_MPAR_FG_COLOR_ENABLED = "white"; var BUTTON_MPAR_FG_COLOR_DISABLED = "silver"; var BUTTON_MPAR_ID_MAIN = "buttonMparMain"; var BUTTON_MPAR_ID_FLOAT = "buttonMparFloat"; var BUTTON_MPAR_TEXT = "mark previous read"; var WAIT_MESSAGE_ID = "mprWaitMessage"; var VIEW_CLASS_TITLES = "u0Entry"; var VIEW_CLASS_MAGAZINE = "u4Entry"; var VIEW_CLASS_CARDS = "u5Entry"; var VIEW_CLASS_FULL = "u100Frame"; var FEEDLY_APP_VER = unsafeWindow.feedlyApplicationVersion; var BROWSER_NAME_CHROME = "Chrome"; var BROWSER_NAME_FIREFOX = "Firefox"; var MSG_NOT_IMPL_FOR_VIEW = "Not implemented for this View."; var MSG_NOT_IMPL_FOR_VIEW_MAG = "Not implemented for the Magazine or Timeline Views."; var MSG_NOT_IMPL_FOR_VIEW_CARDS = "Not implemented for the Cards View."; var MSG_NOT_IMPL_FOR_VIEW_FULL = "Not implemented for the Full Article View."; var MSG_NO_ITEM_SELECTED = "You must first select an item in order to mark previous items as read."; var MSG_ALL_READ = "All items are already marked as read."; var MSG_ERROR_INVALID_VIEW_CLASS = "Error: Invalid view class."; var MSG_ERROR_EXHAUSTED_DOM = "Error: End of DOM reached without finding selcted item."; var MSG_ERROR_INVALID_ITEM = "Error: Invalid item being processed: "; var MSG_ERROR_NULL_ITEM = "Error: Null item being processed."; var MSG_PLEASE_WAIT = "Please wait..."; var DELAY_ITEM_CHECK = 1; // 1/1000th of a second var DELAY_SCROLL_BACK = 1; // 1/1000th of a second var LIST_CONTAINER_ID = "feedlyFrame"; //########################################################################### // G L O B A L S //########################################################################### var isFeedlyTooOld = null; var isFeedlyTooNew = null; var isBrowserInvalid = null; var browserName = null; var buttonBackgroundColor = null; var buttonForegroundColor = null; var titleElementMutationObserver = null; var titleElementMutationObserverInit = null; var scriptName = null; var scriptVersion = null; //########################################################################### // F U N C T I O N S //########################################################################### function markPreviousItemsAsRead() { // If this script isn't compatible with the browser or the installed version of Feedly, don't do anything if (!isCompatible(true)) { return; } // Save the position of the scrolled item list var userScrollPosition = window.pageYOffset; debug("markPreviousItemsAsRead: userScrollPosition: [" + userScrollPosition + "]"); // If the selected item isn't expanded inline AND the selected item isn't selected via keyboard shortcuts... var itemList; var selectedItem; if (!(selectedItem = document.querySelector(".inlineFrame")) && !(selectedItem = document.querySelector(".selectedEntry"))) { // ...Set the selected item to the last read item in the visible item list if ((itemList = getItemList()) !== null) { Array.prototype.slice.call(itemList).forEach(function(item) { if (item.querySelector(".read")) { selectedItem = item; } }); if (!selectedItem) { return; } } else { return; } } debug("markPreviousItemsAsRead: selectedItem: [" + selectedItem.id + "]"); if (!selectedItem) { alert(MSG_NO_ITEM_SELECTED); return; } var selectedItemMasterId = selectedItem.id.split("_")[0] + "_" + selectedItem.id.split("_")[1]; // Get a list of items in the visible item list if ((itemList = getItemList()) === null) { return; } // Declare a hash to keep track of which list items we've already processed as we go through and mark them as read var itemsProcessed = {}; // Hide the item list while we're marking previous items as read hideList(document.getElementById(LIST_CONTAINER_ID)); // This self-executing function kicks off the processing of the items to be marked as read (function markItemAsRead(currentItem) { if (currentItem === null) { showList(document.getElementById(LIST_CONTAINER_ID)); alert(MSG_ERROR_NULL_ITEM); return; } debug("markItemAsRead: ######## currentItem: [" + currentItem.nodeName + " | " + currentItem.id + "]"); // If current item is not a DIV, or doesn't end in "_main", "_main_abstract" or "_inlineframe", try next sibling if (currentItem.nodeName != "DIV" || (!currentItem.id.match(".+_main$") && !currentItem.id.match(".+_main_abstract$") && !currentItem.id.match(".+_inlineframe$"))) { if (currentItem.nextSibling) { debug("markItemAsRead: -------- currentItem not a DIV, or doesn't end in _main, _main_abstract or " + "_inlineframe. Try next sibling: [" + currentItem.nextSibling.nodeName + " | " + currentItem.nextSibling.id + "]"); markItemAsRead(currentItem.nextSibling); } else { showList(document.getElementById(LIST_CONTAINER_ID)); alert(MSG_ERROR_EXHAUSTED_DOM); } return; } // Determine "master" item ID for tracking (e.g., ID "2a2a7bf95c78ac9c_main" has master ID "2a2a7bf95c78ac9c") var currentItemMasterId = null; if (currentItem.id.match(".+_inlineframe$") || currentItem.id.match(".+_main$") || currentItem.id.match(".+_main_abstract$")) { currentItemMasterId = currentItem.id.split("_")[0] + "_" + currentItem.id.split("_")[1]; debug("markItemAsRead: -------- currentItemMasterId: [" + currentItemMasterId + "]"); } else { showList(document.getElementById(LIST_CONTAINER_ID)); alert(MSG_ERROR_INVALID_ITEM + "[" + currentItem.nodeName + " | " + currentItem.id + "]"); return; } // If we've reached the selected item, we're done, so scroll back to the original position and un-hide the list if (currentItemMasterId == selectedItemMasterId) { debug("markItemAsRead: -------- Reached selected item: [" + selectedItem.id + "]"); setTimeout(function() { click(document.getElementById(selectedItemMasterId + "_main")); if (COLLAPSE_SELECTED_ITEM) { setTimeout(function() { click(document.querySelector(".frameActionsTop")); showListAndScroll(LIST_CONTAINER_ID, userScrollPosition); }, DELAY_SCROLL_BACK); return; } showListAndScroll(LIST_CONTAINER_ID, userScrollPosition); }, DELAY_SCROLL_BACK); return; } // If the current item hasn't been read yet and hasn't been processed yet, click it's "main" entry if (!currentItem.querySelector(".read") && !itemsProcessed[currentItemMasterId]) { itemsProcessed[currentItemMasterId] = true; var elementToClick; if ((elementToClick = getElementToClick(currentItem)) !== null) { click(elementToClick); // Click again to close the item so that it will get marked as read (as of Feedly 25.0.919) setTimeout(function() { click(document.querySelector(".frameActionsTop")); }, DELAY_ITEM_CHECK); debug("markItemAsRead: -------- START processing currentItemMasterId [" + currentItemMasterId + "]"); setTimeout(function() { markItemAsRead(currentItem); }, DELAY_ITEM_CHECK * 2); } else { showList(document.getElementById(LIST_CONTAINER_ID)); alert(MSG_ERROR_INVALID_VIEW_CLASS + ": [" + className + "]"); removeMparButtons(); } return; } // Else if the current item is being processed but hasn't been marked read yet, wait a bit and check it again else if (!currentItem.querySelector(".read") && itemsProcessed[currentItemMasterId]) { debug("markItemAsRead: -------- CONTINUE processing currentItemMasterId [" + currentItemMasterId + "]"); setTimeout(function() { markItemAsRead(currentItem); }, DELAY_ITEM_CHECK); return; } // Else we're done with the current item, so go on to the next one else { debug("markItemAsRead: -------- FINISHED processing currentItemMasterId [" + currentItemMasterId + "]"); if (currentItem.nextSibling) { setTimeout(function() { markItemAsRead(currentItem.nextSibling); }, DELAY_ITEM_CHECK); } else { showList(document.getElementById(LIST_CONTAINER_ID)); alert(MSG_ERROR_EXHAUSTED_DOM); return; } return; } })(itemList[0]); } function hideList(listContainer) { // Hide the list listContainer.style.display = "none"; // Create the "Please wait..." message var waitMessage = document.createElement("div"); waitMessage.id = WAIT_MESSAGE_ID; waitMessage.style.backgroundColor = COLOR_FEEDLY_GREEN; waitMessage.style.color = "white"; waitMessage.style.whiteSpace="nowrap" waitMessage.style.fontSize = "3em"; waitMessage.innerHTML = MSG_PLEASE_WAIT; waitMessage.style.width = "7em"; waitMessage.style.height = "1.5em"; waitMessage.style.lineHeight = "1.5em"; waitMessage.style.marginLeft = "auto"; waitMessage.style.marginRight = "auto"; waitMessage.style.position = "relative"; waitMessage.style.textAlign = "center"; waitMessage.style.verticalAlign = "middle"; waitMessage.style.borderRadius = "0.25em"; // Insert the (hidden) message element so that the "used values" will be available for getting the computed style, // then determine the 'top' style value and un-hide the message element waitMessage.style.display = "none"; document.body.insertBefore(waitMessage, document.body.childNodes[0]); var body = document.body var html = document.documentElement; var docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); var waitMessageHeight = parseInt(window.getComputedStyle(waitMessage).height.split("px")); var waitMessageTop = Math.floor(docHeight / 2) - (waitMessageHeight / 2); waitMessage.style.top = waitMessageTop + 'px'; waitMessage.style.display = "block"; } function showList(listContainer) { var waitMessage = document.getElementById(WAIT_MESSAGE_ID); waitMessage.parentNode.removeChild(waitMessage); listContainer.style.display = "block"; } function showListAndScroll(listContainerId, scrollPosition) { showList(document.getElementById(listContainerId)); window.scrollTo(0, scrollPosition); } function getItemList() { var sectionColumn = document.getElementById("section0_column0"); if (sectionColumn !== null) { if (sectionColumn.children.length == 0) { return null; } else { switch (sectionColumn.children[0].className.split(" ")[0].trim()) { case VIEW_CLASS_MAGAZINE: alert(MSG_NOT_IMPL_FOR_VIEW_MAG); removeMparButtons(); return null; case VIEW_CLASS_CARDS: alert(MSG_NOT_IMPL_FOR_VIEW_CARDS); removeMparButtons(); return null; case VIEW_CLASS_FULL: alert(MSG_NOT_IMPL_FOR_VIEW_FULL); removeMparButtons(); return null; } } return sectionColumn.children; } else { alert(MSG_NOT_IMPL_FOR_VIEW); removeMparButtons(); return null; } } function getElementToClick(item) { var className = item.className.split(" ")[0].trim(); debug("getElementToClick: className: [" + className + "]"); switch (className) { case VIEW_CLASS_TITLES: return item; case VIEW_CLASS_MAGAZINE: case VIEW_CLASS_CARDS: case VIEW_CLASS_FULL: default: return null; } } function click(element) { if (element != null) { debug("click: element [" + element.id + "]"); var mouseEvent = document.createEvent("MouseEvents"); mouseEvent.initMouseEvent("click", true, false, Window.self, 1, 0, 0, 0, 0, false, false, false, false, 0, null); element.dispatchEvent(mouseEvent); } } function createMparButton(titleBarId, buttonMparId) { // Create the MPAR button var buttonMpar = document.createElement("span"); buttonMpar.id = buttonMparId; buttonMpar.style.marginLeft = "10px"; buttonMpar.style.backgroundColor = buttonBackgroundColor; buttonMpar.style.color = buttonForegroundColor; buttonMpar.style.whiteSpace="nowrap" buttonMpar.className = "hAction secondary"; buttonMpar.innerHTML = BUTTON_MPAR_TEXT; buttonMpar.addEventListener("click", markPreviousItemsAsRead, true); // Add the MPAR button to the specified title bar, before the span that displays the unread count var feedlyTitleBar = document.getElementById(titleBarId); var buttonInserted = false; Array.prototype.slice.call(feedlyTitleBar.getElementsByTagName("span")).some(function(spanElement) { if (spanElement.className == "hhint") { feedlyTitleBar.insertBefore(buttonMpar, spanElement); buttonInserted = true; return true; } }); // If there is no hint bar when loading a category (which I suspect is a Feedly bug), add the button anyway if (buttonInserted == false) { feedlyTitleBar.appendChild(buttonMpar); } } function removeMparButtons() { document.getElementById(BUTTON_MPAR_ID_MAIN).parentNode.removeChild(document.getElementById(BUTTON_MPAR_ID_MAIN)); document.getElementById(BUTTON_MPAR_ID_FLOAT).parentNode.removeChild(document.getElementById(BUTTON_MPAR_ID_FLOAT)); } function checkCompatibility() { // Check browser validity switch (browserName) { case BROWSER_NAME_CHROME: case BROWSER_NAME_FIREFOX: isBrowserInvalid = false; break; default: isBrowserInvalid = true; break; } isFeedlyTooOld = false; isFeedlyTooNew = false; var feedlyAppVerComponents = FEEDLY_APP_VER.split("\."); // Check if Feedly version is too old var feedlyAppVerMinComponents = FEEDLY_APP_VER_MIN.split("\."); var componentCount = feedlyAppVerComponents.length > feedlyAppVerMinComponents.length ? feedlyAppVerComponents.length : feedlyAppVerMinComponents.length var i = 0; for (i = 0; i < componentCount; i++) { var feedly = feedlyAppVerComponents[i] ? feedlyAppVerComponents[i] : 0; var min = feedlyAppVerMinComponents[i] ? feedlyAppVerMinComponents[i] : 0; if (feedly == min) { continue; } else if (feedly < min) { isFeedlyTooOld = true; return; } else if (feedly > min) { break; } } // Check if Feedly version is too new var feedlyAppVerMaxComponents = FEEDLY_APP_VER_MAX.split("\."); var componentCount = feedlyAppVerComponents.length > feedlyAppVerMaxComponents.length ? feedlyAppVerComponents.length : feedlyAppVerMaxComponents.length var i = 0; for (i = 0; i < componentCount; i++) { var feedly = feedlyAppVerComponents[i] ? feedlyAppVerComponents[i] : 0; var max = feedlyAppVerMaxComponents[i] ? feedlyAppVerMaxComponents[i] : 0; if (feedly == max) { continue; } else if (feedly > max) { isFeedlyTooNew = true; return; } else if (feedly < max) { break; } } } function isCompatible(isWarningDisplayed) { if (isFeedlyTooOld === null || isFeedlyTooNew === null || isBrowserInvalid === null) { checkCompatibility(); } if (isFeedlyTooOld && isWarningDisplayed) { alert("Script '" + scriptName + "' is incompatible with outdated Feedly version " + FEEDLY_APP_VER + "."); } else if (isFeedlyTooNew && isWarningDisplayed) { alert("Script '" + scriptName + "' is incompatible with updated Feedly version " + FEEDLY_APP_VER + ". Check the script's homepage to determine if a newer version is available."); } else if (isBrowserInvalid && isWarningDisplayed) { alert("Script '" + scriptName + "' only runs on " + BROWSER_NAME_CHROME + " and " + BROWSER_NAME_FIREFOX + "."); } if (isFeedlyTooOld || isFeedlyTooNew || isBrowserInvalid) { return false; } else { return true; } } function initialize() { // Get script info when using Greasemonkey or Tampermonkey if (typeof GM_info != 'undefined') { scriptName = GM_info.script.name; scriptVersion = GM_info.script.version; } // Else get script info when using Scriptish or ??? else { scriptName = GM_getMetadata("name");; scriptVersion = GM_getMetadata("version");; } console.log("Loading userscript \"" + scriptName + "\" v" + scriptVersion); if (navigator.userAgent.match(new RegExp(BROWSER_NAME_CHROME))) { browserName = BROWSER_NAME_CHROME; } else if (navigator.userAgent.match(new RegExp(BROWSER_NAME_FIREFOX))) { browserName = BROWSER_NAME_FIREFOX; } if (isCompatible(true)) { buttonBackgroundColor = BUTTON_MPAR_BG_COLOR_ENABLED; buttonForegroundColor = BUTTON_MPAR_FG_COLOR_ENABLED; } else { buttonBackgroundColor = BUTTON_MPAR_BG_COLOR_DISABLED; buttonForegroundColor = BUTTON_MPAR_FG_COLOR_DISABLED; } titleElementMutationObserver = new MutationObserver(function(mutationRecordArray) { // Get the text of the mutated title element titleText = document.querySelector("title").innerHTML; debug("titleElementMutationObserver: titleText: [" + titleText + "]"); // Don't put the MPAR buttons on certain pages if (EXCLUDED_PAGES.some(function(excludedPage) { if (titleText.match(excludedPage)) { return true; } })) { return; } // Create MPAR buttons and add them to the title bars createMparButton("feedlyTitleBar", BUTTON_MPAR_ID_MAIN); createMparButton("floatingTitleBar", BUTTON_MPAR_ID_FLOAT); }); titleElementMutationObserverInit = { childList : true, attributes : true, characterData : true, subtree : true }; } function debug(message) { if (IS_DEBUG_ON) { console.debug(scriptName + ": " + message); } } //########################################################################### // M A I N //########################################################################### // Perform some start-up tasks initialize(); // Observe changes to the title element, informing us that Feedly has started populating the page titleElementMutationObserver.observe(document.querySelector("title"), titleElementMutationObserverInit);