Pawoo Always Show Spoilers

Automatically shows spoilers on media when browsing Pawoo through the web client.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Pawoo Always Show Spoilers
// @namespace    https://github.com/TaleirOfDeynai/
// @version      1.0
// @description  Automatically shows spoilers on media when browsing Pawoo through the web client.
// @author       Taleir
// @match        https://pawoo.net/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // Helper functions.
    function logError(ex) {
        console.error(`[UserScript:Pawoo Always Show Spoilers] Encountered an error; ${ex.message}`);
        console.error(ex);
    }

    function isDetached(node) {
        while (node) {
            if (node === document.documentElement) return false;
            node = node.parentNode;
        }
        return true;
    }

    function createTrial(fn) {
        return function(arg) {
            try { fn(arg); }
            catch (ex) { logError(ex); }
        };
    }

    // Helper classes.
    class Observable {
        constructor(source) {
            this._source = Observable.toSource(source);
        }

        static toSource(obj) {
            switch (true) {
                case typeof obj.forEach === "function":
                    return obj.forEach.bind(obj);
                case typeof obj === "function":
                    return obj;
                default:
                    return (iterator) => iterator(obj);
            }
        }

        static join(...args) {
            args = args.map(Observable.toSource);
            return new Observable(iterator => args.forEach(source => source(iterator)));
        }

        forEach(fn) {
            try { this._source(fn); }
            catch (ex) { logError(ex); }
            return this;
        }

        map(fn) {
            var mappedSource = (iterator) => this._source(val => iterator(fn(val)));
            return new Observable(mappedSource);
        }

        flat() {
            var flattenedSource = (iterator) => this._source(val => {
                Observable.toSource(val)(iterator);
            });
            return new Observable(flattenedSource);
        }

        flatMap(fn) {
            var flatMappedSource = (iterator) => this._source(val => {
                Observable.toSource(fn(val))(iterator);
            });
            return new Observable(flatMappedSource);
        }

        collect(fn) {
            var collectedSource = (iterator) => this._source(val => {
                var result = fn(val);
                if (typeof result !== "undefined") iterator(result);
            });
            return new Observable(collectedSource);
        }

        filter(fn) {
            var filteredSource = (iterator) => this._source(val => fn(val) && iterator(val));
            return new Observable(filteredSource);
        }

        zip(fn) {
            var zippedSource = (iterator) => this._source(left => {
                Observable.toSource(fn(left))(right => {
                    iterator(Array.isArray(left) ? [...left, right] : [left, right]);
                });
            });
            return new Observable(zippedSource);
        }
    }

    class NodeObserver {
        constructor(parent, selector) {
            this.disconnected = false;
            this._callbacks = [];
            this._nodes = new Set();

            var addNode = (added) => {
                if (this._nodes.has(added)) return;
                this._callbacks.forEach(fn => fn(added));
                this._nodes.add(added);
            };

            var tryAddNode = (added) => {
                if (!added.matches) return;
                if (added.matches(selector)) return addNode(added);
                added.querySelectorAll(selector).forEach(addNode);
            };

            // Attach to all existing nodes that match.
            parent.querySelectorAll(selector).forEach(addNode);

            this._observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(tryAddNode);
                    if (mutation.removedNodes.length > 0)
                        for (var node of this._nodes)
                            if (isDetached(node))
                                this._nodes.delete(node);
                });
            });

            this._observer.observe(parent, { childList: true, subtree: true });
        }

        disconnect() {
            if (this.disconnected) return;
            this._observer.disconnect();
            this._callbacks = [];
            this._nodes = new Set();
            this.disconnected = true;
        }

        forEach(fn) {
            if (this.disconnected) return;
            fn = createTrial(fn);
            this._nodes.forEach(fn);
            this._callbacks.push(fn);
        }

        toObservable() {
            return new Observable(this);
        }
    }

    // Inject style for revealed articles.
    var styleNode = document.createElement("style");
    styleNode.innerHTML = `
        .app-body > .app-holder article .status__wrapper.PASS_ext__revealed,
        .app-body > .app-holder .detailed-status__wrapper .status.PASS_ext__revealed,
        .app-body > .app-holder .detailed-status__wrapper .detailed-status.PASS_ext__revealed,
        .container.pawoo-wide .entry.PASS_ext__revealed {
            position: relative;
        }

        .PASS_ext__revealed > * {
            background-color: transparent !important;
        }

        .PASS_ext__revealed::before {
            background-color: rgba(255, 0, 0, 0.05);
            content: '';
            display: block;
            width: 100%;
            height: 100%;
            position: absolute;
            left: 0;
            top: 0;
            pointer-events: none;
        }
    `;
    document.head.appendChild(styleNode);

    // Actual work of the script starts here.
    var applyModifications = (tuple) => {
        tuple[0].setAttribute("data-pass-ext-visited", "");
        tuple[0].classList.add("PASS_ext__revealed");
        tuple[1].click();
    };

    // Observe on the main application entry points.
    var app = new NodeObserver(document.documentElement, `.app-body > .app-holder`).toObservable();
    var act = new NodeObserver(document.documentElement, `.container.pawoo-wide`).toObservable();

    // Observe on the main app's articles list.
    var appArticles = app.flatMap(node => new NodeObserver(node, `.item-list[role="feed"] article .status__wrapper`));

    // Observe on the main app's detailed view.
    var appView = app.flatMap(node => new NodeObserver(node, `.detailed-status__wrapper`));
    var appViewStatus = appView.flatMap(node => new NodeObserver(node, `.status`));
    var appViewDetail = appView.flatMap(node => new NodeObserver(node, `.detailed-status`));

    // Observe on the activity stream entries and detailed views.
    var actEntries = act.flatMap(node => new NodeObserver(node, `.h-feed .entry`));
    var actDetail = act.flatMap(node => new NodeObserver(node, `.h-entry .entry`));

    // Apply the modifications.
    Observable.join(appArticles, appViewStatus, appViewDetail, actEntries, actDetail)
        .zip(node => new NodeObserver(node, `button.media-spoiler`))
        .filter(tuple => !tuple[0].hasAttribute("data-pass-ext-visited"))
        .forEach(applyModifications);
})();