Pawoo Always Show Spoilers

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

Versione datata 08/06/2019. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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