Youtube: Add to Playlist Icons

Adds icons to the video toolbar which adds to specific playlists with 1 click.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            Youtube: Add to Playlist Icons
// @namespace       http://xshade.ca
// @version         6
// @description     Adds icons to the video toolbar which adds to specific playlists with 1 click.
// @icon            https://youtube.com/favicon.ico
// @author          Chris H (Zren / Shade)
// @include         http*://*.youtube.com/*
// @include         http*://youtube.com/*
// @include         http*://*.youtu.be/*
// @include         http*://youtu.be/*
// @grant           GM_addStyle
// ==/UserScript==


(function (window) {
    //--- Settings
    // name is case sensitive
    var addToPlaylistButtons = [
        { name: "Saved", label: "♥" },
        { name: "Music", label: "♫" },
        { name: "Watch Later", label: "⏱" },
    ];

    //---
    var el = function(html) {
        var e = document.createElement('div');
        e.innerHTML = html;
        return e.removeChild(e.firstChild);
    }

    var isWatchUrl = function(url) {
        if (!url)
            url = document.location.href;
        return url.match(/https?:\/\/(www\.)?youtube.com\/watch\?/)
            || url.match(/https?:\/\/(www\.)?youtube.com\/(c|channel)\/[^\/]+\/live/);
    }

    function waitFor(selector, callback) {
        var tick = function(){
            var e = document.querySelector(selector);
            if (e) {
                callback(e);
            } else {
                setTimeout(tick, 100);
            }
        };
        tick();
    }
    window.waitFor = waitFor

    //--- History
    var HistoryEvent = function() {}
    HistoryEvent.listeners = []

    HistoryEvent.dispatch = function(state, title, url) {
      var stack = this.listeners
      for (var i = 0, l = stack.length; i < l; i++) {
        stack[i].call(this, state, title, url)
      }
    }
    HistoryEvent.onPushState = function(state, title, url) {
        HistoryEvent.dispatch(state, title, url)
        return HistoryEvent.origPushState.apply(window.history, arguments)
    }
    HistoryEvent.onReplaceState = function(state, title, url) {
        HistoryEvent.dispatch(state, title, url)
        return HistoryEvent.origReplaceState.apply(window.history, arguments)
    }
    HistoryEvent.inject = function() {
        if (!HistoryEvent.injected) {
            HistoryEvent.origPushState = window.history.pushState
            HistoryEvent.origReplaceState = window.history.replaceState

            window.history.pushState = HistoryEvent.onPushState
            window.history.replaceState = HistoryEvent.onReplaceState
            HistoryEvent.injected = true
        }
    }

    HistoryEvent.timerId = 0
    HistoryEvent.onTick = function() {
        var currentPage = window.location.pathname + window.location.search
        if (HistoryEvent.lastPage != currentPage) {
            HistoryEvent.dispatch({}, document.title, window.location.href)
            HistoryEvent.lastPage = currentPage
        }
    }
    HistoryEvent.startTimer = function() {
        HistoryEvent.lastPage = window.location.pathname + window.location.search
        HistoryEvent.timerId = setInterval(HistoryEvent.onTick, 500)
    }
    HistoryEvent.stopTimer = function() {
        clearInterval(HistoryEvent.timerId)
    }


    //---
    var YoutubeScript = function(name) {
        this.name = name;
    }
    YoutubeScript.prototype.registerMaterialListeners = function() {
        this._materialPageTransition = this.materialPageTransition.bind(this)
        HistoryEvent.listeners.push(this._materialPageTransition);
        HistoryEvent.startTimer();
    }
    YoutubeScript.prototype.materialPageTransition = function() {
        console.log('checkIfWatchPage');
        if (isWatchUrl())
            this.onWatchPage();
    }
    YoutubeScript.prototype.checkIfWatchPage = function() {
        console.log('checkIfWatchPage');
        if (isWatchUrl())
            this.onWatchPage();
    }
    YoutubeScript.prototype.onWatchPage = function() {}


    //---
    var ytScript = new YoutubeScript('ytplaylisticons');
    ytScript.onWatchPage = function() {
        this.injectUI();
    }

    ytScript.injectUI = function() {
        console.log('[playlisticons] injectUI')
        var resizeButton = document.querySelector('.ytp-size-button.ytp-button');
        console.log('[playlisticons] resizeButton', resizeButton)

        waitFor('.ytp-size-button.ytp-button', function(resizeButton){
            console.log('[playlisticons] waitFor')
            console.log('[playlisticons] waitFor.resizeButton', resizeButton)
            if (resizeButton) {
                for (var i = 0; i < addToPlaylistButtons.length; i++) {
                    var playlistData = addToPlaylistButtons[i];
                    var button = document.querySelector('.yt-playlist-enqueue-button[data-playlist-name="' + playlistData.name + '"].ytp-button');
                    if (!button) {
                        button = el('<button class="yt-playlist-enqueue-button ytp-button" data-playlist-name="' + playlistData.name + '" title="Add to ' + playlistData.name + ' Playlist" style="vertical-align: top; text-align: center;">');
                        button.appendChild(el('<span style="font-size: 24px; font-weight: bold;">' + playlistData.label + '</span>'));
                        button.addEventListener('click', ytScript.addToPlaylist(playlistData.name));
                        resizeButton.parentNode.insertBefore(button, resizeButton);
                        //console.log('addToPlaylistButton', playlistData, button);
                    }
                }
            }
        });

        
    }

    ytScript.showPopupTimer = 0
    ytScript.hidePopup = function() {
        clearTimeout(ytScript.showPopupTimer)
        document.body.classList.add('hide-add-to-playlist-popup')
    }
    ytScript.unhidePopup = function() {
        clearTimeout(ytScript.showPopupTimer)
        ytScript.showPopupTimer = setTimeout(function(){
            document.body.classList.remove('hide-add-to-playlist-popup')
        }, 500)
    }

    ytScript.addToPlaylist = function(playlistName) {
        return function() {
            ytScript.hidePopup();

            // button yt-icon svg g path
            var iconPath = document.querySelector('path.yt-icon[d="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"]');
            var button = iconPath.closest('button');
            button.click();

            // ytd-playlist-add-to-option-renderer
            //   paper-checkbox
            //     #checkboxLabel
            //       #checkbox-container
            //         #checkbox-label
            //           yt-formatted-string#label
            //             "Watch Later"
            waitFor('ytd-playlist-add-to-option-renderer', function(e) {
                for (var e2 of document.querySelectorAll('ytd-playlist-add-to-option-renderer')) {
                    var label = e2.querySelector('#checkbox-label yt-formatted-string')
                    console.log(label.textContent, label, e2)
                    if (label.textContent == playlistName) {
                        e2.querySelector('paper-checkbox').click();
                        document.querySelector('#player').click(); // dismiss popup and focus on player
                        ytScript.unhidePopup();
                        break;
                    }
                }
            });
        };
    }

    ytScript.main = function() {
        this.registerMaterialListeners();
        this.checkIfWatchPage();
    };
    ytScript.main();

    var style = ".ytwp-viewing-video #appbar-main-guide-notification-container { top: -110px; left: calc(100vw - 30px - 200px - 30px); width: 200px; text-align: right; }";
    style += '.hide-add-to-playlist-popup ytd-add-to-playlist-renderer { display: none; }';
    style += '.yt-playlist-enqueue-button:active { transform: scale(1.25); }';
    GM_addStyle(style);

})(typeof unsafeWindow !== 'undefined' ? unsafeWindow : window);