YouTube: Hide Watched Videos

Hides watched videos from your YouTube subscriptions page.

当前为 2016-11-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube: Hide Watched Videos
// @namespace    http://www.globexdesigns.com/
// @version      1.0
// @description  Hides watched videos from your YouTube subscriptions page.
// @author       Evgueni Naverniouk
// @grant        GM_addStyle
// @include      http://*.youtube.com/*
// @include      http://youtube.com/*
// @include      https://*.youtube.com/*
// @include      https://youtube.com/*
// ==/UserScript==

// To submit bugs or submit revisions please see visit the repository at:
// https://github.com/globexdesigns/youtube-hide-watched
// You can open new issues at:
// https://github.com/globexdesigns/youtube-hide-watched/issues

(function (undefined) {
    // Enable for debugging
    var __DEV__ = true;

    // Set defaults
    localStorage.YTHWV_WATCHED = localStorage.YTHWV_WATCHED || 'false';
    localStorage.YTHWV_WATCH_PERC = localStorage.YTHWV_WATCH_PERC || '0';

    GM_addStyle(`
.YT-HWV-WATCHED { display: none !important; }

.YT-HWV-CONTAINER {
    display: inline-flex;
    position: relative;
    vertical-align: -2px;
}

.YT-HWV-BUTTON {
    align-items: center;
    background: #F8F8F8;
    border: 1px solid #D3D3D3;
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
    color: #333;
    cursor: pointer;
    display: flex;
    font-size: 11px;
    font-weight: 500;
    height: 28px;
}

.YT-HWV-BUTTON:focus,
.YT-HWV-BUTTON:hover {
    background: #F0F0F0;
    border-color: #C6C6C6;
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.10);
}

.YT-HWV-HIDEBUTTON {
    border-radius: 2px 0 0 2px;
    padding: 0 10px;
}

.YT-HWV-MENUBUTTON {
    border-radius: 0 2px 2px 0;
    border-left: 0;
    padding: 0 10px 0 5px;
}

.YT-HWV-BUTTON-CHECKBOX {
    margin: 0 8px 0 0;
    pointer-events: none;
    vertical-align: 2px;
}

.YT-HWV-MENU {
    background: #F8F8F8;
    border: 1px solid #D3D3D3;
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
    display: none;
    font-size: 12px;
    margin-top: -1px;
    padding: 10px;
    position: absolute;
    right: 0;
    text-align: center;
    top: 100%;
    white-space: normal;
    z-index: 9999;
}

.YT-HWV-MENU-ON { display: block; }
.YT-HWV-MENUBUTTON-ON span { transform: rotate(180deg) }

.YT-HWV-MENU-WATCH-PERC {
    align-items: center;
    display: flex;
    justify-content: center;
    font-size: 11px;
    margin: 5px auto;
}

.YT-HWV-MENU-WATCH-PERC-INPUT {
    cursor: pointer;
    margin: auto 10px;
    vertical-align: -4px;
}
`);

    // ===========================================================

    var addJQuery = function (callback) {
        var script = document.createElement("script");
        script.setAttribute("src", "//code.jquery.com/jquery-3.1.1.slim.min.js");
        script.setAttribute("integrity", "sha256-/SIrNqv8h6QGKDuNoLGA4iret+kyesCkHGzVUUV0shc=");
        script.setAttribute("crossorigin", "anonymous");
        script.addEventListener('load', function() {
            document.body.appendChild(script);
            if (__DEV__) console.log('[YT-HWV] jQuery has been loaded');
            callback();
        }, false);
        document.body.appendChild(script);
    };

    // ===========================================================

    var findWatchedElements = function () {
        var watched = $('.resume-playback-progress-bar')
            .filter(function (i, bar) {
                return bar.style.width && parseInt(bar.style.width, 10) > parseInt(localStorage.YTHWV_WATCH_PERC, 10);
            });

        if (__DEV__) console.log(`[YT-HWV] Found ${watched.length} watched elements`);

        return watched;
    };

    // ===========================================================

    var findParentByClass = function(el, cls) {
        while ((el = el.parentElement) && !el.classList.contains(cls));
        return el;
    };

    // ===========================================================

    var findButtonTarget = function () {
        // Button will be injected into the menu of an item browser
        var target = $('#browse-items-primary .yt-uix-menu-top-level-button-container');

        // If this is a "History" video -- we don't need a button. We use
        // DOM detection here instead of URL detection, because the URL
        // will change before the DOM has been updated.
        if ($('#watch-history-pause-button').length > 0) return;

        return target;
    };

    // ===========================================================

    var isButtonAlreadyThere = function () {
        return $('.YT-HWV-CONTAINER').length > 0;
    };

    // ===========================================================

    var addClassToWatchedRows = function () {
        // Clean up first
        $('.YT-HWV-WATCHED').removeClass('YT-HWV-WATCHED');

        if (localStorage.YTHWV_WATCHED !== 'true') return;

        $(findWatchedElements()).each(function (i, item) {
            // "Subscription" section needs us to hide the "feed-item-container",
            // but in the "Trending" section, that class will hide everything.
            // So there, we need to hide the "expanded-shelf-content-item-wrapper"
            var row;
            if (window.location.href.indexOf('/feed/subscriptions') > 0) {
                row = item.closest('.feed-item-container');
            } else {
                row = item.closest('.expanded-shelf-content-item-wrapper');
            }

            var gridItem = item.closest('.yt-shelf-grid-item');

            // If we're in grid view, we will hide the "grid" item,
            // otherwise we'll hide the item row
            var itemsToHide = gridItem ? $(gridItem) : $(row);

            // If this is the first row in the list, then we can't hide it entirely,
            // otherwise it will also hide the menu. So, we'll have to hide various
            // inner components instead.
            const hasMenu = itemsToHide.find('.menu-container.shelf-title-cell .yt-uix-menu-container').length > 0;
            if (hasMenu) {
                var itemToHide = itemsToHide;
                itemsToHide = itemToHide.find('.expanded-shelf').add(itemToHide.find('.branded-page-module-title'));
            }

            itemsToHide.addClass('YT-HWV-WATCHED');
        });
    };

    // ===========================================================

    var addCheckboxButton = function () {
        if (isButtonAlreadyThere()) return;

        // Find button target
        var target = findButtonTarget();
        if (!target) return;

        // Generate button DOM
        var li = $('<li class="yt-uix-menu-top-level-button yt-uix-menu-top-level-flow-button" />');
        var container = $('<div class="YT-HWV-CONTAINER" />').appendTo(li);
        var button = $('<button class="YT-HWV-BUTTON YT-HWV-HIDEBUTTON">Hide Watched</button>').appendTo(container);
        var checkbox = $('<input class="YT-HWV-BUTTON-CHECKBOX" type="checkbox" />').prependTo(button);
        var menubutton = $('<button class="YT-HWV-BUTTON YT-HWV-MENUBUTTON"><span class="yt-uix-button-arrow yt-sprite" /></button>').appendTo(container);
        var menu = $('<div class="YT-HWV-MENU">Videos are considered "watched" when you have watched at least: </div>').appendTo(container);
        var watchedContainer = $('<div class="YT-HWV-MENU-WATCH-PERC">0%<span />100%</div>').appendTo(menu);
        var watchedInput = $('<input class="YT-HWV-MENU-WATCH-PERC-INPUT" type="range" max="100" min="0" />').appendTo(menu.find('span'));

        // Attach events
        button.on("click", function () {
            var value = localStorage.YTHWV_WATCHED === 'true' ? 'false' : 'true';
            localStorage.YTHWV_WATCHED = value;
            checkbox.attr('checked', value === 'true' ? true : false);
            addClassToWatchedRows();
        });

        menubutton.on("click", function () {
            menubutton.toggleClass("YT-HWV-MENUBUTTON-ON");
            menu.toggleClass("YT-HWV-MENU-ON");
        });

        watchedInput.on("change", function (event) {
            localStorage.YTHWV_WATCH_PERC = event.target.value;
            run();
        });

        // Set DOM values accordingly
        if (localStorage.YTHWV_WATCHED === 'true') checkbox.attr('checked', true);
        watchedInput.attr('value', localStorage.YTHWV_WATCH_PERC);

        // Insert button into DOM
        target.prepend(li);
    };

    var run = function () {
        if (__DEV__) console.log('[YT-HWV] Running check for watched videos');
        addClassToWatchedRows();
        addCheckboxButton();
    };

    // ===========================================================

    // Hijack all XHR calls
    var send = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function (data) {
        this.addEventListener("readystatechange", function () {
            if (
                // Anytime more videos are fetched -- re-run script
                this.responseURL.indexOf('browse_ajax?action_continuation') > 0
            ) {
                setTimeout(function () {
                    run();
                }, 0);
            }
        }, false);
        send.call(this, data);
    };

    // ===========================================================

    var observeDOM = (function() {
        var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
        var eventListenerSupported = window.addEventListener;

        return function(obj, callback) {
            if (MutationObserver) {
                var obs = new MutationObserver(function (mutations, observer) {
                    if (mutations[0].addedNodes.length || mutations[0].removedNodes.length) {
                        callback(mutations);
                    }
                });

                obs.observe(obj, {childList: true, subtree: true});
            } else if (eventListenerSupported) {
                obj.addEventListener('DOMNodeInserted', callback, false);
                obj.addEventListener('DOMNodeRemoved', callback, false);
            }
        };
    })();

    // ===========================================================

    if (__DEV__) console.log('[YT-HWV] Starting Script');
    addJQuery(function () {
        // YouTube does navigation via history and also does a bunch
        // of AJAX video loading. In order to ensure we're always up
        // to date, we have to listen for ANY DOM change event, and
        // re-run our script.
        if (__DEV__) console.log('[YT-HWV] Attaching DOM listener');
        observeDOM(document.body, run);

        run();
    });
}());