Pawoo Always Show Spoilers

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или 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);
})();