BitChute: Add-to-Playlist-Button for all Videos

Adds the BitChute playlist button to every video thumbnail, right next to the 'Watch Later' button.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            BitChute: Add-to-Playlist-Button for all Videos
// @namespace       org.sidneys.userscripts
// @homepage        https://gist.githubusercontent.com/sidneys/23018bf607466ebeb5b11c7889774665/raw/
// @version         0.9.7
// @description     Adds the BitChute playlist button to every video thumbnail, right next to the 'Watch Later' button.
// @author          sidneys
// @icon            https://i.imgur.com/4GUWzW5.png
// @noframes
// @match           *://*.bitchute.com/*
// @require         https://greasyfork.org/scripts/38888-greasemonkey-color-log/code/Greasemonkey%20%7C%20Color%20Log.js
// @require         https://greasyfork.org/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js
// @connect         bitchute.com
// @grant           GM.addStyle
// @grant           GM.download
// @grant           unsafeWindow
// @run-at          document-start
// ==/UserScript==


/**
 * ESLint
 * @global
 */
/* global Debug, onElementReady */
Debug = false


/**
 * Inject Stylesheet
 */
let injectStylesheet = () => {
    console.debug('injectStylesheet')

    GM.addStyle(`
        /* ==========================================================================
           ELEMENTS
           ========================================================================== */

        /* .show-playlist-modal
           ========================================================================== */

        .action-button.show-playlist-modal
        {
            top: 30px;
            font-size: 24px;
            width: 30px;
            height: 30px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .action-button.show-playlist-modal > svg.action-icon:hover
        {
            transform: unset;
        }

        .action-button
        {
            cursor: pointer;
        }
    `)
}


/**
 * Monkey Patch History.pushState() to emit a 'pushstate' event on <window>.
 * @see {@link https://stackoverflow.com/a/4585031/1327892}
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/History/pushState}
 */
const originalPushState = unsafeWindow.history.pushState
unsafeWindow.history.pushState = (state, title = '', url) => {
    console.debug('history.pushState()', 'state:', JSON.stringify(state), 'title:', title, 'url:', url)

    // Create event
    const event = new CustomEvent('pushstate', { 'detail': state, 'bubbles': true, 'cancelable': false })

    // Emit event
    unsafeWindow.dispatchEvent(event)

    // Call original
    originalPushState.call(unsafeWindow.history, state, title, url)
}


/**
 * Register Event Handlers
 */
let registerEventHandlers = () => {
    console.debug('registerEventHandlers')

    // Add BitChute's internal event handlers
    unsafeWindow.playlistAttachEvents()
    unsafeWindow.playlistAttachModalEvents()
    unsafeWindow.spaAttachEvents()
    unsafeWindow.scrollerAttachEvents()

    // Log message after Playlist shown
    unsafeWindow.jQuery('.playlist-modal')
        .off('shown.bs.modal')
        .on('shown.bs.modal', (event) => {
            console.debug('.playlist-modal#shown.bs.modal')

            console.info('Shown', 'Playlist')
        })

    // Log message after Playlist hidden
    unsafeWindow.jQuery('.playlist-modal')
        .off('hidden.bs.modal')
        .on('hidden.bs.modal', (event) => {
            console.debug('.playlist-modal#hidden.bs.modal')

            console.info('Hidden', 'Playlist')
        })
}


/**
 * Render Button 'Add to Playlist'
 * @param {Element} element - Target Element
 */
let renderButtonElement = (element) => {
    console.debug('renderButtonElement')

    // Create Element
    const buttonElement = document.createElement('span')
    buttonElement.className = 'show-playlist-modal'
    buttonElement.innerHTML = `
        <i class="action-icon fas fa-list fa-fw"></i>
    `
    buttonElement.title = 'Add to Playlist'
    buttonElement.dataset.toggle = 'tooltip'
    buttonElement.dataset.placement = 'bottom'
    unsafeWindow.jQuery(buttonElement).data('video', unsafeWindow.jQuery(element).data('video'))

    if (element.classList.contains('action-button')) {
        buttonElement.classList.add('action-button')
    }

    if (element.classList.contains('toolbox-button')) {
        buttonElement.classList.add('toolbox-button')
    }

    // Render Element
    element.after(buttonElement)

    // Status
    console.debug('Rendered', 'Button Element')
}

/**
 * Render Modal 'Playlist'
 * @param {Element} element - Target Element
 */
let renderPlaylistElement = (element) => {
    console.debug('renderPlaylistElement')

    // Abort if exists
    if (document.querySelector('.playlist-modal')) { return }

    // Create Element
    const playlistElement = document.createElement('div')
    playlistElement.setAttribute('role', 'dialog')
    playlistElement.className = 'modal fade playlist-modal'
    playlistElement.innerHTML = `
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
                    <i class="fas fa-times fa-fw"></i>
                </button>
                <a href="/" class="spa">
                    <img class="modal-logo logo-full" src="/static/v130/images/logo-full-day.png" alt="BitChute">
                </a>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <div class="create-playlist-entry-error alert alert-danger hidden"></div>
                    <label>Add to Playlist</label>
                    <span class="options"></span>
                </div>
                <div class="create-playlist-error alert alert-danger hidden"></div>
                <div class="form-group">
                    <form method="post" id="create-playlist-form">
                        <input type="hidden" name="csrfmiddlewaretoken" value="XA3zoHAEsntOaFTteSV3rhIVboyJf5P5HeYjAioyqn2hKQ8IXcBOcKZCTduYIVc4">
                        <label>Create a New Playlist</label>
                        <div class="create-playlist-error-playlist alert alert-danger hidden"></div>
                        <div class="name">
                            <input type="text" name="playlist" class="form-control" autocomplete="off" placeholder="My Playlist" maxlength="100" id="id_playlist">
                            <span class="toolbox-button create" data-video="UkfyMnll0o9O" data-toggle="tooltip" data-placement="bottom" title="Create Playlist">
                                <span class="fa-layers">
                                    <i class="fal fa-square"></i>
                                    <i class="far fa-plus" data-fa-transform="shrink-7"></i>
                                </span>
                            </span>
                        </div>
                        <p class="help">This must be unique. Maximum 100 characters.</p>
                    </form>
                </div>
            </div>
        </div>
    </div>
    `

    // Render Element
    element.before(playlistElement)

    // Status
    console.debug('Rendered', 'Playlist Element')
}


/**
 * Init
 */
let init = () => {
    console.info('init')

    // Add Stylesheet
    injectStylesheet()

    // Add Playlist
    onElementReady('.container', true, (element) => {
        renderPlaylistElement(element)
    })

    // Add Buttons
    onElementReady('.playlist-watch-later', false, (element) => {
        // Render Element
        renderButtonElement(element)

        // Attach Events
        registerEventHandlers()
    })
}


/**
 * @listens document:Event#readystatechange
 */
document.addEventListener('readystatechange', () => {
    console.debug('document#readystatechange', document.readyState)

    if (document.readyState !== 'interactive') { return }

    /**
     * @listens window:Event#jQuery(document).ready
     */
    unsafeWindow.jQuery(document).ready(() => {
        console.debug('window#jQuery(document).ready')

        init()
    })
})

/**
 * Handle in-page navigation on BitChute
 * @listens window:Event#pushstate
 */
unsafeWindow.addEventListener('pushstate', (event) => {
    console.debug('window#pushstate', 'event.detail:', JSON.stringify(event.detail))

    init()
})