Feedly - Mark Previous As Read

Adds a button to Feedly that marks items above the current item "as read".

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);