Reddit++

Improved experience for reddit.com

目前为 2024-05-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         Reddit++
// @name:ru      Reddit++
// @namespace    RedditPlusPlus
// @license      CC-BY-SA-4.0
// @version      0.1.9
// @description  Improved experience for reddit.com
// @description:ru  Улучшение интерфейса reddit.com
// @author       lnm95
// @match        *://www.reddit.com/*
// @icon         data:image/gif;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEf/NABE//QARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//QBE/+8ARP/MAEP/ngBF/1wAIv8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9OAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//gAR/+gAEj/JQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/7AFP/kABI/woAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARf9/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/1wBi/yQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQv+YAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/6wA9/zQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACL/JABF/+sARf//AEX//wBF//8ARf//AEX//wBF//8BRf//E1L9/yVe/P8xZ/v/N2r6/zdq+v8wZvr/Jl77/xNR/f8BRv7/AEX//wBF//8ARf//AEX//wBF//8ARf//AEX/6wAj/yIAAAAAAAAAAAAAAAAAAAAAAAAAAABI/woARf/XAEX//wBF//8ARf//AEX//wBF//8GSf3/K2H3/0l28v9Re+//UXru/1B67f9Qee3/UHnt/1B67f9Qeu7/UXrv/0p28f8pYPf/Bkn9/wBF//8ARf//AEX//wBF//8ARf//AEX/2ABG/wgAAAAAAAAAAAAAAAAAAAAAAFP/jwBF//8ARf//AEX//wBF//8ARf//HFf4/0p17f9Qeez/UHns/1B56/9Qeez/UHrs/1J87/9SfO//UHrt/1B57P9Qeez/UHnr/1B57P9Lde3/HVj4/wBF//8ARf//AEX//wBF//8ARf//AEX/kQAAAAAAAAAAAAAAAABH/yUARf/7AEX//wBF//8ARf//AEX//ydf9f9Qeev/UHrs/1F77v9TfO//U37w/0Zx5f8iTcD/Ejyt/xE7rP8hTL//RXDk/1R+8f9TffD/Unzv/1F77f9Qeez/KF/1/wBF//8ARf//AEX//wBF//8ARf/5AEX/LAAAAAAAAAAAAEP/oABF//8ARf//AEX//wBF//8dWPj/UXvt/1N98P9Uf/P/VoH0/1aB9f9CbuL/CTSo/wgzpv8JNKf/CTSn/wczpv8JNKj/QW3i/1aC9v9WgfT/VYDz/1R/8f9Sfe//H1n4/wBF//8ARf//AEX//wBF//8ARf+gAAAAAAAi/xAARf/4AEX//wBF//8ARf//BEj+/0168v9VgPP/VoL1/1eC9v9Yg/j/WIP5/xA8sv8DLqT/BC+k/wUwpf8FMKX/BC+k/wMupP8PO7D/WIT5/1iD+P9Xg/f/V4L2/1aB9P9Oe/P/BEj+/wBF//8ARf//AEX//wBF//cARf8QAEX/XABF//8ARf//AEX//wBF//8fW/v/VoL1/1eD9/9Yg/j/x83h/+fi2P/Fyt3/VH/0/0x37P9Ic+j/RnHm/0Vx5v9Ic+j/THjt/1R/9P9ahvv/yM7h/+fi2P/Fytz/WIP3/1eC9v8hXPv/AEX//wBF//8ARf//AEX//wBF/10ARf+eAEX//wBF//8ARf//AEX//zRp+v9Yg/j/WYT6/1mF+//x7un/7+vk/+Tf1P9Le/3/W4f+/1uH/v9ch/7/XIf+/1uH/v9bh/7/TX3+/x5e/v/x7un/7+vk/+Tf1P9Zhfr/WIT5/zZr+v8ARf//AEX//wBF//8ARf//AEX/nQBF/8wARf//AEX//wBF//8XVPr/T3z2/1mF+v9ahvv/W4b8//Xy7v/y8Or/5d/U/xZV/v9ciP//XIj//1yI//9ciP//XIj//1yI//8YVv3/AE7///Xy7v/y8Or/5d/U/1qG/P9Zhfv/T3z3/xZU+f8ARf//AEX//wBF//8ARf/QAET97wBF//8ARf//EVD7/1B77/9hifT/nbHp/6G06/+lufL/+Pbz//f18f/r597/eJXn/56w5f+esOX/g6Dw/1yI//9mjvz/nrLr/3yY6v94mvL/+Pbz//f18f/r597/n7Lo/52w5P+breH/eZbk/xBQ+/8ARf//AEX//wBE/u0ARf/9AEX//wBF//84bPj/WIP4/6O49P/y7+n/8/Dr//f28v/8/Pr//Pv5//b07//s6OH/5uHX/+Xf1f/j39f/XIj//6S69v/y7+n/8/Dr//f28v/8/Pr//Pv5//b07//s6OH/5uHX/+Xf1f/j3tf/OW34/wBF//8ARf//AEX//gBF//4ARf//AEX//0d4+/9bh/3/qL/8//z8+v/8/Pr//f38//7+/f/+/vz/+/r3//b07//z8Ov/8/Hr//Hw7/9ciP//qL/9//z8+v/8/Pr//f38//7+/f/+/vz/+/r3//b07//z8Ov/8/Hr//Hw7/9IePv/AEX//wBF//8ARf/9AEX97QBF//8ARf//PXH9/1yI/v9nkP//qcD+/63D/v+tw/7//v79//z7+f/18u7/p7v0/6e89P+qv/n/i6r9/1yI//9nkP//qcD+/63D/v+tw/7//v79//z7+f/18u7/p7v0/6e89P+qv/n/i6r9/z5y/f8ARf//AEX//wBF/u4ARf/QAEX//wBF//8WVf7/W4f//1yI//9ciP//XIj//1yI///+/vz/+Pbz/+rl3P9ciP//XIj//1yI//9ciP//XIj//1yI//9ciP//XIj//1yI///+/vz/+Pbz/+rl3P9ciP//XIj//1yI//9bh///FlX+/wBF//8ARf//AET/zQBG/50ARf//AEX//wBF//8iXv//VoT//1yI//9ciP//U4H///39/P/18+7/5eDV/1yI//9ciP//XIj//1yI//9ciP//XIj//1yI//9ciP//XIj///39/P/18+7/5eDV/1yI//9ciP//V4T//yFd//8ARf//AEX//wBF//8ARf+eAET/XQBF//8ARf//AEX//wBF//8ESP//FlX//xRT//8CR///9/n8//b08P/j39r/UoH//1yI//9ciP//XIj//1yI//9ciP//XIj//1KB//82bP//9/n8//b08P/i39r/FVT//xZV//8ESP//AEX//wBF//8ARf//AEX//wBF/1wARf8QAEX/9wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//9LfP7/fZ76/0d29/8ARf//Ckz//xdW//8UR87/G03T/xdW//8KTP//AEX//wBF//9LfP7/fZ76/0d29/8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/4ACL/EAAAAAAARv+gAEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wU+2f8OPr3/AEX//wBF//8ARf//D0/7/yxi9v8nXvb/B0n9/wBF//8ARf//AEX//wBF//8ARf//AEX//wBH/58AAAAAAAAAAABG/ywARf/5AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AUT3/wo3rv8MRNv/AEX//xZT+v9RfPD/VX/y/1R/8v9JdvH/B0n9/wBF//8ARf//AEX//wBF//8ARf/7AEf/JQAAAAAAAAAAAAAAAABF/5EARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//A0Hn/wo3rf8OPr//JlTQ/1J+8/9ahvz/Wob8/1mF+f8lX/v/AEX//wBF//8ARf//AEX//wBT/5AAAAAAAAAAAAAAAAAAAAAAAEj/CABF/9gARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AUT4/wU+1/8dS8j/Un70/1yI//9ciP7/XIj+/ypj/f8ARf//AEX//wBF//8ARf/XAEj/CgAAAAAAAAAAAAAAAAAAAAAAAAAAAKL/IgBF/+sARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//yJe/v9ciP//XIj//12J//9fiv//DE7//wBF//8ARf//AEX/6wBi/yQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD3/NABF/+sARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//x9c//8/c///PnL//xFS//8ARf//AEX//wBF/+sAPf80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGL/JABF/9cARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/YACP/IgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEP/CgBT/48ARf/7AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/5AEX/kQBI/wgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABI/yUAR/+gAEX/+ABF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf//AEX//wBF//8ARf/3AEX/oABG/ywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo/8QAEX/XABF/54ARf/MAET/7gBF//0ARf/+AEX/7QBF/88ARv+dAEX/XQBF/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAD/4AAAP/AAAA/4AAAH/AAAA/gAAAHwAAAA8AAAAOAAAABgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABwAAAA8AAAAPgAAAH8AAAD/gAAB/8AAA//wAA///AA/8=
// @run-at document-end
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==


(function() {
    'use strict';


    const REVISION = 5;

    const pp_app = document.body.querySelector(`shreddit-app`);
    if(pp_app == null || pp_app.getAttribute(`devicetype`) != `desktop`)
    {
        console.log(`Reddit++ was stopped for a non compatible page`);
        return;
    }

    let pp_meta = document.head.querySelector(`meta[name="reddit-plus-plus"]`);
    if(pp_meta != null)
    {
        console.log(`Reddit++ already runned`);
        return;
    }

    pp_meta = document.createElement(`meta`);
    pp_meta.setAttribute(`name`, `reddit-plus-plus`);
    pp_meta.setAttribute(`rev`, REVISION);
    document.head.append(pp_meta);



    // ***********************************************************************************************************************
    // ************************************************** CLASSES ************************************************************
    // ***********************************************************************************************************************

    class Database
    {
        constructor(name, isCleanupable) {
            this.databaseKey = name+`_DATABASE`;
            this.refreshKey = name+`_REFRESHED`;
            this.cleanupKey = name+`_CLEANUP`;
            this.isCleanupable = (isCleanupable != undefined && isCleanupable == true);
            this.refresh();

            // cleanup database
            if(this.isCleanupable && GM_getValue(this.cleanupKey, 0) < Date.now())
            {
                // limit data storage for 1 day
                const timestampLimit = Date.now() - (1000 * 60 * 60 * 24);

                this.data = Object.fromEntries(Object.entries(this.data).filter(([key, value]) => value.timestamp > timestampLimit) );
                this.refreshed = Date.now();

                GM_setValue(this.databaseKey, this.data);
                GM_setValue(this.refreshKey, this.refreshed);
                GM_setValue(this.cleanupKey, Date.now() + (1000 * 60 * 10));
            }
        }

        refresh()
        {
            const lastRefreshed = GM_getValue(this.refreshKey, 0);
            if(this.data == undefined || this.refreshed < lastRefreshed)
            {
                this.refreshed = lastRefreshed;
                this.data = GM_getValue(this.databaseKey, {});
            }
        }

        get(id)
        {
            this.refresh();

            const data = this.data[id];

            return (data == undefined) ? {} : data;
        }

        isDisabled(id)
        {
            return this.get(id) == false;
        }

        isEnabled(id)
        {
            return !this.isDisabled(id);
        }

        set(id, value)
        {
            this.refresh();

            if(this.isCleanupable)
            {
                value.timestamp = Date.now();
            }

            this.data[id] = value;
            this.refreshed = Date.now();

            GM_setValue(this.databaseKey, this.data);
            GM_setValue(this.refreshKey, this.refreshed);
        }
    }

    class CustomCSS
    {
        constructor()
        {
            this.stylesheet = new CSSStyleSheet();
            this.registry(document);
        }

        registry(source)
        {
            source.adoptedStyleSheets.push(this.stylesheet);
        }

        addRule(rule)
        {
            this.stylesheet.insertRule(rule, 0);
        }

        addVar(name, lightValue, darkValue)
        {
            this.addRule(`:root.theme-light { ${name}: ${lightValue} !important;}`);
            this.addRule(`:root { ${name}: ${darkValue ?? lightValue};}`);
        }
    }

    class Window
    {
        constructor(tittle, render, onClose) {
            this.tittle = tittle;
            this.render = render;
            this.onClose = onClose;
            this.container = null;
            this.content = null;
            this.closeButton = null;
        }

        build()
        {
            this.container = document.createElement(`div`);
            this.container.classList.add(`pp_WindowContainer`);
            this.container.addEventListener('click', e => { if(e.target == this.container) { this.close(); }});

            const win = AppendNew(this.container, `div`, `pp_Window`);

            const tittleContainer = AppendNew(win, `div`, `pp_WindowTittleContainer`);

            let tittle = AppendNew(tittleContainer, `div`, [`pp_WindowTittle`, `flex`, `flex-row`]);
            tittle = AppendNew(tittle, `span`, [`text-24`, `font-semibold`]);
            tittle.textContent = this.tittle;


            this.closeButton = AppendNew(tittleContainer, `div`, [`pp_WindowCloseButton`, `flex`, `items-center`]);
            this.closeButton = AppendNew(this.closeButton, `button`, [`button`, `icon`, `inline-flex`, `items-center`, `justify-center`, `button-small`, `button-secondary`, `px-[var(--rem6)]`]);
            this.closeButton.setAttribute(`tittle`, `Close ${this.tittle}`);
            this.closeButton.addEventListener('click', e => { this.close(); });

            this.closeButton = AppendNew(this.closeButton, `span`, [`flex`, `items-center`, `justify-center`]);
            this.closeButton = AppendNew(this.closeButton, `span`, [`flex`]);

            const svg = buildSvg(20, 20, closeWindowGraphic, {c:'none'});
            svg.setAttribute(`height`, `16`);
            svg.setAttribute(`width`, `16`);
            this.closeButton.append(svg);

            AppendNew(win, `hr`, `border-b-neutral-border-weak`); //`border-0`, `border-b-sm`, `border-solid`, `border-b-neutral-border-weak`

            this.content = AppendNew(win, `div`, `pp_WindowContent`);

            AppendNew(win, `div`, `pp_WindowFooter`).textContent = ` `;
        }

        open(context)
        {
            if(this.container == null)
            {
                this.build();
            }

            while (this.content.firstChild) {
                this.content.removeChild(this.content.lastChild);
            }

            this.render(this, context);

            document.body.appendChild(this.container);
            document.body.style.overflow = 'hidden';
        }

        close()
        {
            this.container.remove();

            document.body.style.overflow = 'visible';

            if(this.onClose != undefined)
            {
                this.onClose();
            }
        }
    }

    class ImageViewer
    {
        constructor()
        {
            this.openned = false;
            this.viewer = null;
            this.container = null;
            this.image = null;

            this.mouse = {x:0, y:0};
            this.drag = { enabled:false, start: {x:0, y:0}, current: {x:0, y:0, scale:1} };

            this.scrollImage = this.scrollImage.bind(this);
            this.startDrag = this.startDrag.bind(this);
            this.mouseMove = this.mouseMove.bind(this);
            this.endDrag = this.endDrag.bind(this);
        }

        open(src)
        {
            if(this.openned) return;
            this.openned = true;

            if(this.viewer == null)
            {
                this.build();
            }

            this.image.src = src;


            window.addEventListener('wheel', this.scrollImage, { passive: false });
            this.image.addEventListener('mousedown', this.startDrag);
            document.addEventListener('mousemove', this.mouseMove );
            this.image.addEventListener('mouseup', this.endDrag);
            this.image.addEventListener('mouseleave', this.endDrag);

            // reset pos
            this.drag.current = {x:0, y:0, scale:1};

            this.updateTransform();

            document.body.appendChild(this.viewer);
        }

        close()
        {
            this.viewer.remove();

            this.drag.enabled = false;

            this.container.classList.toggle(`pp_imageViewer_drag`, false);

            window.removeEventListener('wheel', this.scrollImage, { passive: false });
            this.image.removeEventListener('mousedown', this.startDrag);
            document.removeEventListener('mousemove', this.mouseMove);
            this.image.removeEventListener('mouseup', this.endDrag);
            this.image.removeEventListener('mouseleave', this.endDrag);

            this.openned = false;
        }

        build()
        {
            this.viewer = document.createElement(`div`);
            this.viewer.classList.add(`pp_imageViewer`);
            this.viewer.dataset.open = false;

            const closeButton = AppendNew(this.viewer, `div`, `pp_imageViewer_closeButton`);

            const closeSvg = buildSvg(40, 40, closeImageGraphic, {w:1});
            closeButton.appendChild(closeSvg);

            this.container = AppendNew(this.viewer, `div`, `pp_imageViewer_imageContainer`);

            this.image = AppendNew(this.container, `img`, `pp_imageViewer_image`);
            this.image.alt = `Comment image`;
            this.image.ondragstart = function() { return false; };


            // close
            this.viewer.addEventListener('click', (e) => {
                if(e.target != this.image)
                {
                    this.close();
                }
            });

            closeButton.addEventListener('click', () => { this.close(); });
        }

        updateTransform()
        {
            this.container.style.transform = `translate(${this.drag.current.x}px, ${this.drag.current.y}px) scale(${this.drag.current.scale}, ${this.drag.current.scale})`;
        }

        startDrag(event)
        {
            this.drag.start.x = event.screenX - this.drag.current.x;
            this.drag.start.y = event.screenY - this.drag.current.y;
            this.drag.enabled = true;

            this.container.classList.toggle(`pp_imageViewer_drag`, true);
        }

        mouseMove(event)
        {
            this.mouse.x = event.clientX;
            this.mouse.y = event.clientY;

            if(this.drag.enabled)
            {
                this.drag.current.x = event.screenX - this.drag.start.x;
                this.drag.current.y = event.screenY - this.drag.start.y;

                this.updateTransform();
            }
        }

        endDrag()
        {
            this.fit(1);

            this.drag.enabled = false;

            this.container.classList.toggle(`pp_imageViewer_drag`, false);
        }

        scrollImage(e)
        {
            const m = Math.max(1.0, 1.0 + Math.log2(this.drag.current.scale * this.drag.current.scale));

            const prevScale = this.drag.current.scale;

            this.drag.current.scale = Math.max(0.5, this.drag.current.scale + (-e.deltaY / 1000) * m );

            const rect = this.image.getBoundingClientRect();

            const hh = rect.height / 2;
            const hw = rect.width / 2;

            const dy = rect.y + hh;
            const dx = rect.x + hw;

            const os = ( this.drag.current.scale / prevScale - 1);
            this.drag.current.y -= Math.min(Math.max(this.mouse.y - dy, -hh), hh) * os;
            this.drag.current.x -= Math.min(Math.max(this.mouse.x - dx, -hw), hw) * os;


            if(e.deltaY > 0)
            {
                this.drag.current.y /= 1.1;
                this.drag.current.x /= 1.1;
            }


            this.fit(0.33);

            e.preventDefault();
        }

        fit(force)
        {
            const offset = 0;
            const rect = this.image.getBoundingClientRect();

            const left = offset - rect.left;
            const right = rect.right - window.innerWidth + offset;

            if(left > 0 && right < 0)
            {
                this.drag.current.x += ((rect.width > window.innerWidth) ? -right : left) * force;
            }
            else if(left < 0 && right > 0)
            {
                this.drag.current.x += ((rect.width > window.innerWidth) ? left : -right) * force;
            }

            const top = offset - rect.top;
            const bottom = rect.bottom - window.innerHeight + offset;

            if(top > 0 && bottom < 0)
            {
                this.drag.current.y += ((rect.height > window.innerHeight) ? -bottom : top) * force;
            }
            else if(top < 0 && bottom > 0)
            {
                this.drag.current.y += ((rect.height > window.innerHeight) ? top : -bottom) * force;
            }

            this.updateTransform();
        }

    }





    // ***********************************************************************************************************************
    // **************************************************** VARS *************************************************************
    // ***********************************************************************************************************************

    const HOUR_SECONDS = 60 * 60;
    const DAY_SECONDS = HOUR_SECONDS * 24;


    const settingsButtonGraphic = 'M15.07,2.25a.33.33,0,0,1,.33.33V6.72h1.25a2.11,2.11,0,0,1,0,4.21H15.4v4.14a.33.33,0,0,1-.33.33H10.93v1.25a2.11,2.11,0,0,1-4.21,0V15.4H2.58a.33.33,0,0,1-.33-.33v-3A3.51,3.51,0,0,0,4.49,8.82,3.48,3.48,0,0,0,2.25,5.57v-3a.33.33,0,0,1,.33-.33H5.34a3.49,3.49,0,0,0,7,0h2.76m0-1.25H10.75A2.24,2.24,0,1,1,6.9,1H2.58A1.58,1.58,0,0,0,1,2.58v4a2.24,2.24,0,1,1,0,4.47v4a1.58,1.58,0,0,0,1.58,1.58H5.47a3.36,3.36,0,0,0,6.71,0h2.89a1.58,1.58,0,0,0,1.58-1.58V12.18a3.36,3.36,0,0,0,0-6.71V2.58A1.58,1.58,0,0,0,15.07,1Z';
    const settingsGraphic = [{d:'M10 20c-.401 0-.802-.027-1.2-.079a1.145 1.145 0 0 1-.992-1.137v-1.073a.97.97 0 0 0-.627-.878A.98.98 0 0 0 6.1 17l-.755.753a1.149 1.149 0 0 1-1.521.1 10.16 10.16 0 0 1-1.671-1.671 1.149 1.149 0 0 1 .1-1.523L3 13.906a.97.97 0 0 0 .176-1.069.98.98 0 0 0-.887-.649H1.216A1.145 1.145 0 0 1 .079 11.2a9.1 9.1 0 0 1 0-2.393 1.145 1.145 0 0 1 1.137-.992h1.073a.97.97 0 0 0 .878-.627A.979.979 0 0 0 3 6.1l-.754-.754a1.15 1.15 0 0 1-.1-1.522 10.16 10.16 0 0 1 1.673-1.676 1.155 1.155 0 0 1 1.522.1L6.1 3a.966.966 0 0 0 1.068.176.98.98 0 0 0 .649-.887V1.216A1.145 1.145 0 0 1 8.8.079a9.129 9.129 0 0 1 2.393 0 1.144 1.144 0 0 1 .991 1.137v1.073a.972.972 0 0 0 .628.878A.977.977 0 0 0 13.905 3l.754-.754a1.152 1.152 0 0 1 1.522-.1c.62.49 1.18 1.05 1.671 1.671a1.15 1.15 0 0 1-.1 1.522L17 6.1a.967.967 0 0 0-.176 1.068.98.98 0 0 0 .887.649h1.073a1.145 1.145 0 0 1 1.137.991 9.096 9.096 0 0 1 0 2.392 1.145 1.145 0 0 1-1.137.992h-1.073A1.041 1.041 0 0 0 17 13.905l.753.755a1.149 1.149 0 0 1 .1 1.521c-.49.62-1.05 1.18-1.671 1.671a1.149 1.149 0 0 1-1.522-.1L13.906 17a.97.97 0 0 0-1.069-.176.981.981 0 0 0-.65.887v1.073a1.144 1.144 0 0 1-.99 1.137A9.431 9.431 0 0 1 10 20Zm-.938-1.307a7.638 7.638 0 0 0 1.875 0v-.982a2.292 2.292 0 0 1 3.853-1.6l.693.694a8.796 8.796 0 0 0 1.326-1.326l-.694-.694a2.29 2.29 0 0 1 1.6-3.851h.982a7.746 7.746 0 0 0 0-1.876h-.982a2.213 2.213 0 0 1-2.034-1.4 2.223 2.223 0 0 1 .438-2.451l.694-.693a8.76 8.76 0 0 0-1.327-1.326l-.692.694a2.22 2.22 0 0 1-2.434.445 2.221 2.221 0 0 1-1.419-2.041v-.979a7.638 7.638 0 0 0-1.875 0v.982a2.213 2.213 0 0 1-1.4 2.034 2.23 2.23 0 0 1-2.456-.438l-.693-.694a8.757 8.757 0 0 0-1.326 1.327l.694.692a2.216 2.216 0 0 1 .445 2.434 2.22 2.22 0 0 1-2.041 1.418h-.982a7.746 7.746 0 0 0 0 1.876h.982a2.213 2.213 0 0 1 2.034 1.4 2.223 2.223 0 0 1-.438 2.451l-.694.693c.394.488.838.933 1.326 1.326l.694-.694a2.218 2.218 0 0 1 2.433-.445 2.22 2.22 0 0 1 1.418 2.041v.983ZM10 13.229a3.23 3.23 0 1 1 0-6.458 3.23 3.23 0 0 1 0 6.458Zm0-5.208a1.979 1.979 0 1 0 0 3.958 1.979 1.979 0 0 0 0-3.958Z'}];
    const closeWindowGraphic = [{d:'m18.442 2.442-.884-.884L10 9.116 2.442 1.558l-.884.884L9.116 10l-7.558 7.558.884.884L10 10.884l7.558 7.558.884-.884L10.884 10l7.558-7.558Z'}];
    const linkGraphics = [{d:'M14.111 12.5a3.701 3.701 0 0 1-1.09 2.41c-.479.47-.928.922-1.378 1.373-.45.45-.894.9-1.368 1.366a3.852 3.852 0 0 1-2.698 1.099 3.852 3.852 0 0 1-2.698-1.099 3.738 3.738 0 0 1-1.116-2.659c0-.997.402-1.953 1.116-2.658.479-.472.928-.923 1.378-1.375.45-.45.893-.9 1.368-1.365A3.936 3.936 0 0 1 9.638 8.59a3.968 3.968 0 0 1 2.24.258c.27-.269.546-.54.812-.806l.131-.13a5.086 5.086 0 0 0-3.182-.624A5.052 5.052 0 0 0 6.732 8.71c-.48.471-.929.922-1.377 1.373-.449.451-.894.9-1.37 1.366A4.982 4.982 0 0 0 2.5 14.992c0 1.328.534 2.602 1.486 3.543A5.13 5.13 0 0 0 7.58 20a5.13 5.13 0 0 0 3.595-1.465c.478-.471.927-.923 1.377-1.374.451-.451.894-.9 1.368-1.366a4.993 4.993 0 0 0 1.263-2.071c.243-.781.288-1.61.132-2.412L14.11 12.5Z'}, {d:'M16.017 1.467A5.123 5.123 0 0 0 12.422 0a5.123 5.123 0 0 0-3.595 1.467c-.478.471-.926.923-1.377 1.374-.45.451-.894.9-1.367 1.366a4.966 4.966 0 0 0-1.106 1.624 4.907 4.907 0 0 0-.291 2.86l1.2-1.19a3.699 3.699 0 0 1 1.092-2.41c.478-.472.928-.923 1.377-1.374.45-.45.894-.9 1.368-1.366a3.844 3.844 0 0 1 2.698-1.101c1.012 0 1.982.396 2.698 1.101a3.736 3.736 0 0 1 1.116 2.66c0 .996-.401 1.953-1.116 2.658-.478.471-.927.922-1.377 1.373-.45.451-.893.9-1.368 1.367a3.933 3.933 0 0 1-2.014 1.003 3.966 3.966 0 0 1-2.24-.26c-.273.274-.551.549-.818.818l-.123.12a5.087 5.087 0 0 0 3.183.624 5.053 5.053 0 0 0 2.906-1.423c.477-.472.926-.923 1.376-1.375.45-.452.894-.9 1.368-1.365A4.977 4.977 0 0 0 17.5 5.008a4.977 4.977 0 0 0-1.488-3.543l.005.002Z'}];
    const newUserGraphic = [{w:'2', f:'none', d:'M12.5,11.5a3.39,3.39,0,0,1,2,2,3.16,3.16,0,0,1,0,2'}, {w:'0', d:'M1.46,1.5S3.49,4.89,7,5.07c1.49.07,3.35.25,4.06.79,1.41,1.09,2.3,2.08,1.74,4.37a4.91,4.91,0,0,1-4.36,3.49C5.08,14,2.89,10.29,2.33,9.35.41,6.12,1.46,1.5,1.46,1.5Z'}];
    const closeImageGraphic = [{d:'m33.16001,9.52439l-2.37671,-2.37671l-10.691,10.691l-10.691,-10.691l-2.37671,2.37671l10.691,10.691l-10.691,10.691l2.37671,2.37671l10.691,-10.691l10.691,10.691l2.37671,-2.37671l-10.691,-10.691l10.691,-10.691z'}];

    const feedGraphics =
          {
              Best:[{c:'none', d:'M14.51,6.56,9.13,1.17a1.61,1.61,0,0,0-2.26,0L1.49,6.56a1,1,0,0,0,.72,1.73H5.52V14A1.28,1.28,0,0,0,6.8,15.3H9.23A1.29,1.29,0,0,0,10.52,14V8.29h3.27A1,1,0,0,0,14.51,6.56Z'}],
              Hot:[{w:'0.5', d:'M8.49,2.93c.7,1.56,3,2.81,3.69,3.52a5.14,5.14,0,0,1,1.36,5.45c-1.09,3.37-4.49,3.38-6.21,3.38s-4.18-.28-5-3,.8-4.41,1-5,1.06,2.52,2,3.12c1.19.79,2.85,0,2.85-1.18S6.72,7.65,6.44,5.37a10.59,10.59,0,0,1,1-4.9S7.83,1.46,8.49,2.93Z'}],
              New:[{f:'none', w:'1.5', d:'M7.5,3 L7.5,9 L12,6'}, {w:'0.5', d:'M8,15.5A7.5,7.5,0,1,1,15.5,8,7.5,7.5,0,0,1,8,15.5Zm0-14A6.5,6.5,0,1,0,14.5,8,6.51,6.51,0,0,0,8,1.5Z'}],
              Top:[{f:'none', w:'1.5', d:'M14.51,6.56,9.13,1.17a1.61,1.61,0,0,0-2.26,0L1.49,6.56a1,1,0,0,0,.72,1.73H5.52V14A1.28,1.28,0,0,0,6.8,15.3H9.23A1.29,1.29,0,0,0,10.52,14V8.29h3.27A1,1,0,0,0,14.51,6.56Z'}],
              Rising:[{f:'none', w:'2', d:'M8.5,4.5l4.68-2.45S14,5.31,14.5,7.5'}, {f:'none', w:'2',d:'M1.1,14.67c1.4-2.8,3.62-6.84,3.62-6.84L9,12.18l4.13-9.94'}]
          };

    const tagGraphics =
          {
              Followed : [{d:'m0.43678,5.49532l4.46205,-0.8817l2.13151,-3.90349l2.39985,3.78849l4.1937,0.9967l-3.65339,3.14905l1.13489,4.59352l-4.19007,-2.03743l-3.96007,2.03743l0.86655,-4.78519l-3.38505,-2.95738l0,-0.00001l0.00001,0z'}],
              Liked : [{d:'m5.10986,11.44595c-3.39081,-2.55084 -4.60316,-4.16445 -4.61299,-6.1398c-0.00896,-1.80077 1.48763,-3.53879 3.03857,-3.52877c0.77447,0.00501 2.43577,0.6678 3.02404,1.20648c0.29641,0.27142 0.4368,0.24465 1.09671,-0.20908c1.79601,-1.23486 3.54983,-1.26078 4.68568,-0.06924c1.81534,1.90433 1.48483,4.17844 -0.95008,6.53732c-1.29454,1.25411 -4.12153,3.47898 -4.4205,3.47898c-0.09098,0 -0.92862,-0.57415 -1.86143,-1.27588l0,0l0,-0.00001z'}],
              Warning : [{d:'m0.74313,12.0159l6.28126,-9.83543l6.28126,9.83543l-12.56252,0z'}, {d:'m6.91754,5.38201l0,4.06927'}, {d:'m6.91754,10.29973l0,0.7153'}],
              Blocked : [{d:'M3.11,3l7.94,8'}, {d:'M7,12.88A5.88,5.88,0,1,1,12.91,7,5.88,5.88,0,0,1,7,12.88Z'}]
          };

    const tagButtonGraphics =
          {
              Followed: [{d:'m1.56177,7.99265l5.62517,-1.11153l2.68713,-4.92101l3.02542,4.77603l5.28687,1.25651l-4.60571,3.96991l1.43072,5.79091l-5.28229,-2.56852l-4.99233,2.56852l1.09243,-6.03254l-4.26742,-3.72827l0,-0.00001z'}],
              Liked: [{d:'m7.28935,15.93702c-4.80908,-3.55432 -6.52852,-5.80272 -6.54246,-8.55516c-0.0127,-2.50919 2.10985,-4.93093 4.30951,-4.91697c1.09841,0.00698 3.45457,0.93051 4.2889,1.6811c0.42039,0.37819 0.6195,0.3409 1.55543,-0.29133c2.54723,-1.72064 5.03461,-1.75676 6.64556,-0.09648c2.57463,2.65349 2.10589,5.82222 -1.34747,9.10906c-1.836,1.74747 -5.84543,4.84759 -6.26946,4.84759c-0.12903,0 -1.31703,-0.80001 -2.64001,-1.7778l0,0l0,-0.00001z'}],
              Warning: [{d:'m1.24313,16.49297l8.78125,-13.75l8.78125,13.75l-17.5625,0z'}, {d:'m9.875,7.5l0,4.5'}, {d:'m9.875,14.09375l0,1'}],
              Blocked: [{d:'m9.78126,18.09375c-4.41989,0 -8,-3.58011 -8,-8c0,-4.41989 3.58011,-8 8,-8c4.41989,0 8,3.58011 8,8c0,4.41989 -3.58011,8 -8,8z'}, {d:'m4.43767,4.59392l10.81217,10.93716'}]
          };

    const USERTAG_FOLLOWED = `Followed`;
    const USERTAG_LIKED = `Liked`;
    const USERTAG_WARNING = `Warning`;
    const USERTAG_BLOCKED = `Blocked`;

    const TAG_CONFIG_PRIORITY = 0;
    const TAG_CONFIG_HINT = 1;
    const TAG_CONFIG_HINT_ACTIVATED = 2;
    const TAG_CONFIG_COLOR = 3;

    const tagConfigs =
          {
              Followed:[100, `Follow`, `Unfollow`, `#0b7ed3`],
              Liked:[2, `Tag as liked`, `Remove liked tag`, `#C95A54`],
              Warning:[1, `Tag as warned`, `Remove warned tag`, `#D4A343`],
              Blocked:[0, `Block`, `Unblock`, `#663988`]

          };

    const FEED_BUTTONS_EXTENDED = [`Best`, `Hot`, `New`, `Top`, `Rising`];
    const FEED_BUTTONS = [`Hot`, `New`, `Top`, `Rising`];


    const SETTING_WIDE_MODE = `wideMode`;

    const SETTING_SIDEBAR_CUSTOMS = `sidebarCustoms`;
    const SETTING_SIDEBAR_RECENT = `sidebarRecent`;
    const SETTING_SIDEBAR_SUBS = `sidebarSubs`;
    const SETTING_SIDEBAR_RESOURCES = `sidebarResources`;

    const SETTING_FEED_BUTTONS = `feedButtons`;
    const SETTING_FLAIR_BAR = `flairbar`;
    const SETTING_BACKPLATES = `backplates`;

    const SETTING_USER_INFO = `userInfo`;
    const SETTING_USER_TAGS = `userTags`;
    const SETTING_SHOW_NAMES = `showNames`;
    const SETTING_BIGGER_FONTS = `biggerFonts`;
    const SETTING_HIDE_SHARE = `hideShare`;
    const SETTING_GHOSTED_COMMENTS = `ghostedComments`;
    const SETTING_COLLAPSE_AUTOMODERATOR = `collapseAutomoderator`;


    const css = new CustomCSS();

    let settingsRevision = getSettingsRevision();

    let settings = new Database(`SETTINGS`);
    let tags = new Database(`TAGS`);
    let users = new Database(`USERS`, true);
    let flairs = new Database(`FLAIRS`);
    let subs = new Database(`SUBS`, true);

    let flairsWindow = new Window(`Subreddit flairs settings`, renderFlairsWindow);
    let settingsWindow = new Window(`Reddit++ settings`, renderSettingsWindow);

    let imageViewer = new ImageViewer();



    
    // ***********************************************************************************************************************
    // ************************************************ COMMON CSS ***********************************************************
    // ***********************************************************************************************************************

    css.addVar('--color-neutral-background-transparent', '#fff0', '#0b141600');
    css.addVar('--stickiedColor', '#0e8a001c', '#0e8a001c');
    css.addVar('--stickiedHoverColor', '#18900b3d', '#18900b3d');


    // windows
    css.addRule('.pp_WindowContainer { cursor:pointer; position: fixed; top:0px;  z-index: 10; width:100%; height:100%; display: flex; justify-content: center; align-items: center; background-color: #000000b3; }');
    css.addRule('.pp_Window { cursor: auto; display: flex; flex-direction: column; width:900px; height: fit-content; min-height: 200px; max-height: 75%; border-radius: 15px; background-color: var(--color-neutral-background); box-shadow: 0px 0px 50px 0px #00000070; }');
    css.addRule('.pp_WindowTittleContainer { height: 48px; margin: 1rem; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }');
    css.addRule('.pp_WindowTittle { margin-left: 1rem; }');
    css.addRule('.pp_WindowCloseButton { margin: 1rem; }');
    css.addRule('.pp_WindowContent { display: flex; flex-direction: column; overflow-y: overlay; }');
    css.addRule('.pp_WindowFooter { height: 2rem; min-height: 2rem; }');

    css.addRule('.pp_WindowElementsContainer { display: flex; flex-direction: column; padding: 0px; margin: 20px 40px; gap: 0.5rem;}');
    css.addRule('.pp_WindowElement { display: flex; flex-direction: row; justify-content: flex-end; height: 3rem; }');
    css.addRule('.pp_WindowElementsContainer > .pp_WindowElement:hover { background-color: var(--color-neutral-background-hover); border-radius: 15px;}');

    // image viewer
    css.addRule('.pp_imageViewer { display: flex; justify-content: center; align-items: center; position: fixed; top:0px;  z-index: 10; cursor:pointer;  width:100%; height:100%; background-color: #000000b3; }');
    css.addRule('.pp_imageViewer_closeButton { display: flex; align-items: center; justify-content: center; position: fixed; top:50px; z-index: 11; right:50px; cursor:pointer; width:50px; height:50px; color: #ffffff9c; background-color: #00000069; border-radius:30px; }');
    css.addRule('.pp_imageViewer_closeButton:hover { color: #ffffffc7; }');
    css.addRule('.pp_imageViewer_imageContainer {display: flex; justify-content: center; align-items: center; width:80%; height:90%;}');
    css.addRule('.pp_imageViewer_imageContainer:not(.pp_imageViewer_drag) { transition: transform 0.5s;}');
    css.addRule('.pp_imageViewer_image {cursor: grab; object-fit: scale-down; max-width:100%; max-height:100%; box-shadow: 0px 0px 20px 3px #14141485; }');
    css.addRule('.pp_imageViewer_image:active {cursor: grabbing; }');

    // ban hint
    css.addRule('faceplate-banner { max-width: 1000px !important;}');


    if(settings.isEnabled(SETTING_BIGGER_FONTS))
    {
        // resize fonts
        css.addRule(':is(.text-14):not(.pp_defaultText .text-14) { font-size: 1.0rem !important; line-height: 1.4rem !important;}');
        css.addRule('faceplate-hovercard .text-12 { font-size: 0.9rem !important; font-color: #595959 !important;}');

        // comments more space
        css.addRule('shreddit-comment-action-row { margin-bottom: 15px !important;}');
    }


    // hide native share
    if(settings.isEnabled(SETTING_HIDE_SHARE))
    {
        css.addRule('shreddit-comment-share-button { display:none !important;}');
    }


    // posts gradient selection
    if(settings.isEnabled(SETTING_BACKPLATES))
    {
        //[view-context="SubredditFeed"]
        css.addRule('article > shreddit-post { background-color: #00000000 !important; padding-top:10px !important; margin-top: 10px !important; margin-bottom: 10px !important;}'); //border-radius: 0px !important;

        css.addRule('article > shreddit-post::before { border-radius: 15px !important; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; opacity: 0; z-index: -1;  background: linear-gradient(var(--color-neutral-background-hover), var(--color-neutral-background)); transition: opacity 0.2s;}');
        css.addRule('article > shreddit-post:hover::before { opacity: 1;}');

        // override gold
        css.addRule('shreddit-post[gold-count]:not(shreddit-post[gold-count=""]) { background-image: linear-gradient(rgba(255, 214, 53, 0.2), rgba(255, 214, 53, 0)) !important;}');
        css.addRule('shreddit-post[gold-count]:not(shreddit-post[gold-count=""])::before { background: linear-gradient(#fbed2966, var(--color-neutral-background)) !important;}');

        // stickied
        css.addRule('.stickied::after { border-radius: 15px !important; position: absolute; content: ""; top: 0; right: 0; bottom: 0; left: 0; opacity: 1; z-index: -2;  background: linear-gradient(var(--stickiedColor), var(--color-neutral-background)) !important;}');
        css.addRule('.stickied::before { background: linear-gradient(var(--stickiedHoverColor), var(--color-neutral-background)) !important;}');

    }





    // ***********************************************************************************************************************
    // ************************************************ Events ***************************************************************
    // ***********************************************************************************************************************

    // Settings changes
    window.addEventListener("storage", (event) => {
        const currentRevision = getSettingsRevision();

        if(currentRevision != settingsRevision)
        {
            settingsRevision = currentRevision;
            document.addEventListener("visibilitychange", () => { window.location.reload(); }, {once:true});
        }
    });


    // Intersections
    const commentRenderIntersector = new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting)
            {
                renderComment(entry.target.parentNode);

                commentRenderIntersector.unobserve(entry.target);
            }
        }

    }, { threshold: 0.05});


    // Mutations
    new MutationObserver(mutations => {
        const hasComments = window.location.href.includes(`/comments/`);

        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {

                if (node instanceof HTMLElement)
                {
                    // check sidebar`s sections

                    if(node.matches(`reddit-recent-pages`))
                    {
                        awaitShadowrootSection(node, SETTING_SIDEBAR_RECENT);
                    }

                    if(node.matches(`faceplate-expandable-section-helper`))
                    {
                        const customs = node.querySelector(`summary[aria-controls="multireddits_section"]`);
                        if(customs != null)
                        {
                            awaitSection(node, customs, SETTING_SIDEBAR_CUSTOMS);
                        }

                        const subs = node.querySelector(`summary[aria-controls="communities_section"]`);
                        if(subs != null)
                        {
                            awaitSection(node, subs, SETTING_SIDEBAR_SUBS);
                        }
                    }

                    // check new posts
                    if(node.matches(`faceplate-batch`))
                    {
                        node.querySelectorAll(`shreddit-post`).forEach((post) => {
                            renderPost(post);
                        });
                    }

                    // check new comments
                    if(hasComments)
                    {
                        // post tree
                        if(node.id != null && node.id.includes(`comment-tree`) == true)
                        {
                            registryAllComments(node);
                        }

                        // single thread
                        if(node.matches(`shreddit-comment-tree`) == true)
                        {
                            registryAllComments(node);
                        }

                        // more comments
                        if(node.matches(`shreddit-comment`) == true)
                        {
                            registryComment(node);

                            registryAllComments(node);
                        }
                    }


                    // check refresh header
                    if(node.matches(`reddit-header-large`) == true)
                    {
                        renderHeader(node.parentNode);
                    }

                    // check refresh sub pages
                    if(node.matches(`dsa-transparency-modal-provider`) == true)
                    {
                        renderApp(node);

                        renderFeedButtons(node);

                        renderSub(node);

                        checkNativePosts(node);
                    }

                    // check refresh main pages
                    if(node.classList.contains(`grid-container`) && node.querySelector(`.main-container`) != null)
                    {
                        renderApp(node);

                        renderFeedButtons(node);

                        checkNativePosts(node);
                    }
                }
            }
        }
    }).observe(document.body, {childList: true, subtree: true});








    // ***********************************************************************************************************************
    // ************************************************ CORE APP *************************************************************
    // ***********************************************************************************************************************

    if(settings.isEnabled(SETTING_WIDE_MODE))
    {
        css.addRule('@media (min-width: 1392px) { .pp_pageContainer { margin-right: 300px;}}');
        css.addRule('@media (min-width: 1392px) { .pp_mainFeed { width: 700px !important;  }}');
        css.addRule('@media (min-width: 1392px) { .pp_rightSidebar { grid-column-start: 3;  order: 10;}}');

        css.addRule('@media (min-width: 1392px) { #right-sidebar-container { position: fixed; right: 0px;  margin: 15px 10px 0px 0px; } }');
    }


    renderApp(document.body);

    function renderApp(container)
    {
        if(settings.isDisabled(SETTING_WIDE_MODE)) return;


        const leftSidebar = container.querySelector(`#left-sidebar-container`);
        leftSidebar.classList.add(`pp_defaultText`);

        // Firefox first-load fix
        if(awaitElement(leftSidebar, renderApp)) return;

        const resources = leftSidebar.querySelector(`summary[aria-controls="RESOURCES"]`);
        if(resources != null)
        {
            renderSidebarSection(resources.parentNode.parentNode, resources, null, SETTING_SIDEBAR_RESOURCES);
        }

        const pageContainer = leftSidebar.parentNode;
        pageContainer.classList.add(`pp_pageContainer`);

        pageContainer.querySelector(`.subgrid-container`).classList.add(`pp_mainFeed`);

        const rightSidebar = container.querySelector(`#right-sidebar-container`);
        rightSidebar.classList.add(`pp_rightSidebar`);
        rightSidebar.classList.toggle(`styled-scrollbars`, true);

        const originContainer = rightSidebar.parentNode;



        let isWideMode = !(window.innerWidth >= 1392);

        applyRightBar();
        window.addEventListener("resize", (event) => { applyRightBar(); });

        function applyRightBar()
        {
            if(window.innerWidth >= 1392 && !isWideMode)
            {
                pageContainer.prepend(rightSidebar);

                isWideMode = true;
            }

            if(window.innerWidth < 1392 && isWideMode)
            {
                originContainer.append(rightSidebar);
                isWideMode = false;
            }
        }
    }




    // ***********************************************************************************************************************
    // ************************************************ SIDEBAR **************************************************************
    // ***********************************************************************************************************************

    css.addRule('.pp_loadingSection { max-height: 0px !important; visibility: hidden ! important;}');
    css.addRule('.pp_collapsedSection { max-height: 43px !important; overflow-y: hidden ! important;}');

    function awaitShadowrootSection(container, settingName)
    {
        container.classList.add(`pp_loadingSection`);

        const recentId = setInterval(() => {
            const helper = container.shadowRoot?.querySelector(`faceplate-expandable-section-helper`);
            const button = helper?.querySelector(`summary`);

            if(button != null && helper.getAttribute(`open`) != null)
            {
                clearInterval(recentId);

                css.registry(container.shadowRoot);
                helper.classList.add(`pp_defaultText`);

                container.classList.remove(`pp_loadingSection`);
                renderSidebarSection(helper, button, container.querySelector(`hr`), settingName);
            }
        }, 10);
    }

    function awaitSection(container, button, settingName)
    {
        container.classList.add(`pp_loadingSection`);
        const recentId = setInterval(() => {
            if(container.getAttribute(`open`) != null)
            {
                clearInterval(recentId);
                container.classList.remove(`pp_loadingSection`);
                renderSidebarSection(container, button, container.nextElementSibling, settingName);
            }
        }, 10);
    }

    function renderSidebarSection(container, button, hr, settingName)
    {
        const collapsedSettingName = settingName + `Collapsed`;

        if(settings.isEnabled(settingName))
        {
            const details = container.querySelector(`details`);

            if(!settings.isEnabled(collapsedSettingName))
            {
                container.toggleAttribute(`open`, false);
                details.classList.add(`pp_collapsedSection`);
            }

            button.addEventListener(`click`, (e) => {
                const isCollapsed = !(e.currentTarget.getAttribute(`aria-expanded`) === "false");
                settings.set(collapsedSettingName, isCollapsed);
                details.classList.toggle(`pp_collapsedSection`, false);
            });

        }
        else
        {
            container.remove();
            hr?.remove();
        }
    }





    // ***********************************************************************************************************************
    // *********************************************** HEADER **************************************************************
    // ***********************************************************************************************************************

    css.addRule('#reddit-logo { text-decoration: none; }');
    css.addRule('.pp_logo { width: max-content; color: var(--shreddit-color-wordmark); font-size: 22px; font-weight: 1000; letter-spacing: -2px; }');

    css.addRule('.pp_userPanel { grid-column: span3 / span3 !important; }');

    renderHeader(document.body);

    function renderHeader(container)
    {
        const nav = container.querySelector(`reddit-header-large`).querySelector(`nav`);

        const userPanel = nav.childNodes.item(4);

        // Firefox first-load fix
        if(awaitElement(userPanel, renderHeader)) return;

        if(checkIsRendered(nav)) return;


        userPanel.classList.add(`pp_userPanel`);
        userPanel.addEventListener(`click`, () => { renderSettingsButton(); }, {once:true});


        const logo = container.querySelector(`#reddit-logo`);
        const logoPP = AppendNew(logo, `div`, `pp_logo`);
        logoPP.textContent = `++`;
    }


    function renderSettingsButton()
    {
        let userMenu = document.getElementById(`user-drawer-content`);

        if(userMenu.querySelector(`faceplate-tracker[noun="pp-settings"]`) != null)
        {
            return;
        }

        let originSettingsButton = userMenu.querySelector(`faceplate-tracker[noun="settings"]`);

        let ppSettingsButton = originSettingsButton.cloneNode(true);
        ppSettingsButton.setAttribute(`noun`, `pp-settings`);

        originSettingsButton.parentNode.appendChild(ppSettingsButton);

        ppSettingsButton.querySelector(`a`).removeAttribute(`href`);

        let path = ppSettingsButton.querySelector(`path`);
        path.setAttribute(`d`, settingsButtonGraphic);

        let text = ppSettingsButton.querySelector(`.text-14`);
        text.textContent = `Reddit++`;

        settingsWindow.onClose = () => {
            if(settingsWindow.changes > 0)
            {
                nextSettingsRevision();

                window.location.reload();
            }
        };

        ppSettingsButton.addEventListener(`click`, () => { settingsWindow.open(); });
    }






    // ***********************************************************************************************************************
    // ********************************************** FEED BUTTONS ***********************************************************
    // ***********************************************************************************************************************

    css.addRule('.pp_feedButtonsContainer { justify-content: space-between; height: 40px !important; }');

    css.addRule('.pp_feedButton { gap: 0.25rem !important;}');
    css.addRule('.pp_feedContainer { gap: 0.25rem; height: 40px; }');


    renderFeedButtons(document.body);

    function renderFeedButtons(container)
    {
        if(settings.isDisabled(SETTING_FEED_BUTTONS)) return;

        const main = container.querySelector(`.main-container`)?.querySelector(`main`);

        // Firefox first-load fix
        if(awaitElement(main, renderFeedButtons)) return;


        const feedDropdown = main.querySelector(`shreddit-sort-dropdown`);

        // skip non feed page
        if((feedDropdown == null || feedDropdown.getAttribute(`trigger-id`) == `comment-sort-button`) && !window.location.href.includes(`/about/`)) return;
        if(window.location.href.includes(`/user/`)) return;

        // get current feed
        const currentFeed = feedDropdown?.querySelector(`div[slot="selected-item"]`)?.textContent;
        feedDropdown?.remove();


        const isHome = window.location.href.includes(`?feed=home`) || window.location.href == `https://www.reddit.com/`;
        const isPopular = window.location.href.includes(`reddit.com/r/popular/`);
        const isAll = window.location.href.includes(`reddit.com/r/all/`);


        // get container
        let buttonsContainer = null;
        let hrefGenerator = null;
        if(isHome || isPopular || isAll)
        {
            const originContainer = main.querySelector(`shreddit-layout-event-setter`).parentNode;
            originContainer.classList.add(`pp_feedButtonsContainer`);

            buttonsContainer = document.createElement(`div`);
            buttonsContainer.classList.add(`flex`, `mx-md`, `shrink-0`, `pp_feedContainer`);
            originContainer.prepend(buttonsContainer);

            if(isHome)
            {
                hrefGenerator = (feed) => { return `/${feed.toLowerCase()}/?feed=home`; };
            }
            else
            {
                hrefGenerator = (feed) => { return `/r/${isPopular ? `popular` : `all`}/${feed.toLowerCase()}/`; };
            }
        }
        else
        {
            const aboutContainer = main.querySelector(`a[slot="page-3"]`)?.parentNode?.parentNode;

            if(aboutContainer == null)
            {
                console.log(`FLAIR BAR WAS CORRUPTED`);
                setTimeout(() => {renderFeedButtons(container); }, 100 );
                return;
            }

            buttonsContainer = document.createElement(`div`);
            buttonsContainer.classList.add(`flex`, `mx-md`, `shrink-0`, `pp_feedContainer`);
            aboutContainer.before(buttonsContainer);

            aboutContainer.remove();


            const subName = getCurrentSub();
            hrefGenerator = (feed) => { return `/r/${subName}/${feed.toLowerCase()}/`; };
        }


        const feeds = (isHome || isPopular) ? FEED_BUTTONS_EXTENDED : FEED_BUTTONS;

        for(const feed of feeds)
        {
            const button = AppendNew(buttonsContainer, `a`, [`inline-flex`, `flex-col`, `text-neutral-content-weak`, `font-semibold`, `rounded-full`, `hover:no-underline`, `hover:bg-secondary-background-hover`, `hover:text-secondary-content`, `active:bg-secondary-background`, `pl-[var(--rem16)]`, `pr-[var(--rem16)]`]);
            button.href = hrefGenerator(feed);

            const isCurrent = feed == currentFeed;
            button.classList.toggle(`bg-secondary-background-selected`, isCurrent);
            button.classList.toggle(`!text-neutral-content-strong`, isCurrent);

            const spanContainer = AppendNew(button, `span`, [`inline-flex`, `flex-row`, `items-center`, `gap-xs`, `py-[var(--rem10)]`, `leading-5`, `font-14`, `pp_feedButton`]);

            let graphic = feedGraphics[feed];
            if(graphic != null)
            {
                let svg = buildSvg(16, 16, graphic);

                spanContainer.append(svg);
            }

            const spanText = AppendNew(spanContainer, `span`);
            spanText.textContent = feed;
        }
    }



    // ***********************************************************************************************************************
    // ************************************************** SUBS ***************************************************************
    // ***********************************************************************************************************************


    renderSub(document.body);

    function renderSub(container)
    {
        // skip page without feed
        const checkIsFeed = container.querySelector(`.main-container`)?.querySelector(`main`)?.querySelector(`shreddit-feed-error-banner`);
        if(checkIsFeed == null) return;

        renderMasthead();

        // flairBar
        awaitSubInfo();
    }


    css.addRule('.masthead > section > div { display: flex; flex-direction: column; align-items: flex-start;}');
    css.addRule('.masthead > section > div > div:last-child { align-self: flex-end;}');

    function renderMasthead()
    {
        const masthead = document.body.querySelector(`.masthead`);

        if(masthead == null) return;

        const section = masthead.querySelector(`section`);
        section.style.top= `-3rem`;
        const div = section.querySelector(`div`);
        div.style.gap = `1rem`;


    }


    function awaitSubInfo()
    {
        const sub = getCurrentSub();
        const subData = subs.get(sub);

        if(subData.flairs != undefined)
        {
            renderSubFlairs(sub);
            return;
        }

        fetch(`https://www.reddit.com/r/${sub}/api/link_flair_v2.json?raw_json=1`, {
            method: `get`
        })
            .then(r => r.json())
            .then(result => {

            if(result != null)
            {
                const loadedFlairs = [];

                for (const loadedFlair of result)
                {
                    const flair = { text:loadedFlair.text, color:loadedFlair.text_color, background:loadedFlair.background_color, richtext: loadedFlair.richtext};

                    loadedFlairs.push(flair);
                }

                subData.flairs = loadedFlairs;


                subs.set(sub, subData);

                renderSubFlairs(sub);
            }
            else
            {
                console.log(`Reddit++: Unable to load subreddit flairs list`);
            }
        });
    }

    css.addRule('.pp_flairMenuContainer { }');
    css.addRule('.pp_flairMenu { display: flex; flex-direction: row; overflow: hidden; }');
    css.addRule('.pp_flairUL { margin-top: 5px !important; flex-wrap: nowrap !important; position: relative; }'); //transition: left 0.2s;

    css.addRule('.pp_flairUL-smoothed { transition: left 0.1s ease-out;}');

    css.addRule('.pp_flairBorderContainer { width: 100%; display: flex; justify-content: space-between; }');

    css.addRule('.pp_flairBorderPrev { width: 20px; }');
    css.addRule('.pp_flairBorder { z-index: 1; position: absolute; height: 40px; width: 20px; margin-top: 5px; }');
    css.addRule('.pp_flairBorderLeft { background: linear-gradient(90deg, var(--color-neutral-background), 60%, var(--color-neutral-background-transparent));}');
    css.addRule('.pp_flairBorderRight { background: linear-gradient(270deg, var(--color-neutral-background), 60%, var(--color-neutral-background-transparent));}');

    css.addRule('.pp_flairSettings { color: #434849; padding-top:0.4rem !important; cursor:pointer;}');


    function renderFlair(conatiner, sub, flair)
    {
        const a = AppendNew(conatiner, `a`, `no-decoration`);
        a.href = `/r/`+sub+`/?f=flair_name%3A%22`+flair.text+`%22`;

        const span = AppendNew(a, `span`, [`bg-tone-4`, `inline-block`, `truncate`, `max-w-full`, `text-12`, `font-normal`, `box-border`, `px-[6px]`, `rounded-[20px]`, `leading-4`, `max-w-full`, `py-xs`, `!px-sm`, `leading-4`, `h-xl`, `inline-flex`]);
        span.classList.add(flair.color == `light` ? `text-global-white` : `text-global-black`);
        span.style.backgroundColor = flair.background;

        for(const richElement of flair.richtext)
        {
            if(richElement.e == `text`)
            {
                const content = document.createTextNode(richElement.t);
                span.appendChild(content);
            }
            if(richElement.e == `emoji`)
            {
                const fimg = document.createElement(`faceplate-img`);
                fimg.classList.add(`flair-image`);
                fimg.setAttribute(`loading`, `lazy`);
                fimg.setAttribute(`width`, 16);
                fimg.setAttribute(`height`, 16);
                fimg.setAttribute(`src`, richElement.u);
                fimg.setAttribute(`alt`, richElement.a);
                span.appendChild(fimg);
            }
        }
    }


    function renderSubFlairs(sub)
    {
        if(settings.isDisabled(SETTING_FLAIR_BAR)) return;

        const feedContent = document.body.querySelector(`report-flow-provider`)?.querySelector(`.main-container`)?.querySelector(`shreddit-title`)?.parentNode;

        // skip render for non feed page
        if(feedContent == null) return;

        const prevFlairMenu = feedContent.parentNode.querySelector(`.pp_flairMenuContainer`)
        if(prevFlairMenu != null)
        {
            prevFlairMenu.remove();
        }

        // load data
        const subData = subs.get(sub);
        const flairsData = flairs.get(sub);

        // skip render when sub haven't flairs
        if(subData.flairs == undefined || subData.flairs.length == 0) return;




        const flairMenuContainer = document.createElement(`div`);
        flairMenuContainer.classList.add(`pp_flairMenuContainer`);
        feedContent.before(flairMenuContainer);

        const flairMenu = AppendNew(flairMenuContainer, `div`, `pp_flairMenu`);


        const ul = AppendNew(flairMenu, `ul`, [`p-0`, `m-0`, `list-none`, `gap-xs`, `flex`, `flex-row`, `pp_flairUL`]);
        let flairsRendered = 0;

        for (const flair of subData.flairs)
        {
            if(flairsData.hidden != undefined && flairsData.hidden.includes(flair.text)) continue;

            const li = AppendNew(ul, `li`, `max-w-full`);

            renderFlair(li, sub, flair);


            flairsRendered++;
        }

        // prevent render empty menu
        if(flairsRendered == 0)
        {
            flairMenuContainer.remove();
            return;
        }


        // borders
        const borderContainer = document.createElement(`div`);
        borderContainer.classList.add(`pp_flairBorderContainer`);
        flairMenuContainer.prepend(borderContainer);

        const borderLeftC = AppendNew(borderContainer, `div`, `pp_flairBorderPrev`);
        const borderLeft = AppendNew(borderLeftC, `div`, [`pp_flairBorder`, `pp_flairBorderLeft`]);
        borderLeft.textContent = ` `;
        const borderRightC = AppendNew(borderContainer, `div`, `pp_flairBorderPrev`);
        const borderRight = AppendNew(borderRightC, `div`, [`pp_flairBorder`, `pp_flairBorderRight`]);
        borderRight.textContent = ` `;



        const hr = document.createElement(`hr`);
        hr.classList.add(`border-0`, `border-b-sm`, `border-solid`, `border-b-neutral-border-weak`);
        flairMenuContainer.prepend(hr);

        const mymx = document.createElement(`div`);
        mymx.classList.add(`my-xs`, `mx-2xs0`);
        flairMenuContainer.prepend(mymx);


        // navigation
        ul.style.left = `25px`;
        const ulRect = ul.getBoundingClientRect();
        const menuRect = flairMenu.getBoundingClientRect();

        flairMenu.addEventListener(`mousemove`, e => {onMoveOverFlairs(e, ul, flairMenu)});

        if(ulRect.width > menuRect.width * 1.72)
        {
            ul.classList.add(`pp_flairUL-smoothed`);
        }
    }


    function onMoveOverFlairs(e, ul, flairMenu)
    {
        const ulRect = ul.getBoundingClientRect();
        const menuRect = flairMenu.getBoundingClientRect();

        if(ulRect.width < menuRect.width)
        {
            ul.style.left = `25px`;
            return;
        }

        let scale = (e.clientX - (menuRect.x + 25)) / ((menuRect.right - 25) - (menuRect.x + 25));
        scale = Math.max(0, Math.min(scale, 1));

        ul.style.left = `${Math.round(25 - (ulRect.width - menuRect.width + 50) * scale)}px`;
    }


    //************ Flairs Settings *************

    css.addRule('.pp_WindowScrollContent { overflow-y: scroll;  }');

    css.addRule('.pp_checkBoxContainer { float: right; position: relative; }');

    css.addVar('--checkBox-background', '#1a1a1b1a', '#81818152');
    css.addRule('.pp_checkBoxButtonActive { justify-content: flex-end; background-color: #0079d3; }');
    css.addRule('.pp_checkBoxButton { position: relative; cursor:pointer; overflow:visible; display:flex; justify-content: start; background: transparent; background-color: var(--checkBox-background); padding: initial;  height: 24px; width:37.5px; border-radius: 100px; border: 2px solid transparent; transition: background-color 0.2s linear; }');

    css.addRule('.pp_checkBoxKnob { height: 19.5px; width:19.5px; background-color: #fff;  box-shadow: 0 0 0 1px rgba(0,0,0,.1),0 2px 3px 0 rgba(0,0,0,.2); transition: 0.5s linear; border-radius: 57%; }');


    css.addRule('.pp_fs_flairContainer { width: 100%; display: flex; align-items: center; margin-left: 3rem; }');
    css.addRule('.pp_fs_flairPanelCheckboxArea { width: 200px; min-width: 200px; display: flex; justify-content: center; align-items: center; }');

    css.addRule('.pp_fs_columnTittle { margin: 20px 57px 10px 40px; }');



    function renderFlairsWindow(win, context)
    {
        const titlePanel = AppendNew(win.content, `div`, [`pp_WindowElement`, `pp_fs_columnTittle`]);

        const tittleBarArea = AppendNew(titlePanel, `div`, `pp_fs_flairPanelCheckboxArea`);
        const tittleBar = AppendNew(tittleBarArea, `div`, [`text-14`, `font-semibold`, `mb-xs`]);
        tittleBar.textContent = `show on bar:`;
        const tittleFeedArea = AppendNew(titlePanel, `div`, `pp_fs_flairPanelCheckboxArea`);
        const tittleFeed = AppendNew(tittleFeedArea, `div`, [`text-14`, `font-semibold`, `mb-xs`]);
        tittleFeed.textContent = `show in feed:`;


        const scroll = AppendNew(win.content, `div`, [`pp_WindowScrollContent`, `styled-scrollbars`]);

        const elements = AppendNew(scroll, `div`, `pp_WindowElementsContainer`);

        const subData = subs.get(context.sub);
        for (const flair of subData.flairs)
        {
            const panel = AppendNew(elements, `div`, `pp_WindowElement`);

            const flairContainer = AppendNew(panel, `div`, `pp_fs_flairContainer`);

            addFlairToggle(`hidden`);
            addFlairToggle(`banned`);


            function addFlairToggle(target)
            {
                const checkboxArea = AppendNew(panel, `div`, `pp_fs_flairPanelCheckboxArea`);
                const checkBoxContainer = AppendNew(checkboxArea, `div`, `pp_checkBoxContainer`);
                const checkBoxBack = AppendNew(checkBoxContainer, `button`, `pp_checkBoxButton`);
                const initState = !(flairs.get(context.sub)[target]?.includes(flair.text) ?? false);
                checkBoxBack.classList.toggle(`pp_checkBoxButtonActive`, initState);
                const knob = AppendNew(checkBoxBack, `div`, `pp_checkBoxKnob`);


                checkBoxBack.addEventListener(`click`, e => {
                    const flairData = flairs.get(context.sub);

                    let state = flairData[target]?.includes(flair.text) ?? false;

                    checkBoxBack.classList.toggle(`pp_checkBoxButtonActive`, state);

                    if(state)
                    {
                        flairData[target] = flairData[target].filter(f => f != flair.text);
                    }
                    else
                    {
                        const targetList = flairData[target] ?? [];
                        targetList.push(flair.text);
                        flairData[target] = targetList;
                    }

                    flairs.set(context.sub, flairData);
                });
            }

            // flair
            renderFlair(flairContainer, context.sub, flair);

        }
    }



    // *********** Sub ContextMenu *************

    document.body.addEventListener(`click`, clickSubredditMenu);

    function clickSubredditMenu(e)
    {
        if(e.target.matches(`shreddit-subreddit-header-buttons`) == true)
        {
            if(checkIsRendered(e.target)) return;

            const controlMenu = e.target.shadowRoot.querySelector(`shreddit-subreddit-overflow-control`).shadowRoot.querySelector(`faceplate-menu`);

            const originButton = controlMenu.querySelector(`li`);

            // flairs settings
            const menuFlairsButton = originButton.cloneNode(true);
            menuFlairsButton.querySelector(`.text-14`).textContent = `Flairs settings`;
            controlMenu.prepend(menuFlairsButton);

            const sub = getCurrentSub();

            flairsWindow.onClose = () => { renderSubFlairs(sub); };

            menuFlairsButton.addEventListener(`click`, () => { flairsWindow.open({sub:sub}); });

            // about
            const link = document.createElement(`a`);
            link.href = `https://www.reddit.com/`+e.target.getAttribute(`prefixed-name`)+`/about/`;
            link.classList.add(`no-underline`);
            controlMenu.prepend(link);

            const menuAboutButton = originButton.cloneNode(true);
            menuAboutButton.querySelector(`.text-14`).textContent = `About`;
            link.prepend(menuAboutButton);
        }
    }




    // ***********************************************************************************************************************
    // ************************************************* POSTS ***************************************************************
    // ***********************************************************************************************************************

    css.addRule('.pp_bannedPost { visibility: hidden !important;  max-height: 0px;}');


    checkNativePosts(document.body);

    function checkNativePosts(container)
    {
        const main = container.querySelector(`main`);

        if(main != null)
        {
            main.querySelectorAll(`shreddit-post`).forEach((post) => {
                renderPost(post);
            });
        }

    }

    function renderPost(post)
    {
        const sub = post.getAttribute(`subreddit-prefixed-name`).replace(`r/`, ``);
        const flairData = flairs.get(sub);

        const postFlair = post.querySelector(`shreddit-post-flair`)?.querySelector(`a`);
        if(postFlair != null)
        {
            const postFlairText = decodeURIComponent(postFlair.href.split(`%22`)[1]);

            if(flairData.banned != undefined && flairData.banned.includes(postFlairText))
            {
                const next = post.parentElement.nextElementSibling;
                next.classList.add(`pp_bannedPost`);
                post.parentElement.classList.add(`pp_bannedPost`);
                post.classList.add(`pp_bannedPost`);
                post.querySelector(`faceplate-tracker[source="post_credit_bar"]`).classList.add(`pp_bannedPost`);
            }
        }
    }







    // ***********************************************************************************************************************
    // *********************************************** COMMENTS **************************************************************
    // ***********************************************************************************************************************

    const NEWUSER_SECONDS_SHIFT = DAY_SECONDS * 64;
    const REFRESH_USER_SECONDS_SHIFT = DAY_SECONDS;
    const BLOCK_COOLDOWN_SECONDS = DAY_SECONDS + 42;

    let loadingComments = {};
    let currentLoads = 0;


    function registryAllComments(container)
    {
        container.querySelectorAll(`shreddit-comment`).forEach((comment) => {
            registryComment(comment);
        });
    }


    function registryComment(comment)
    {
        if(checkIsRendered(comment)) return;

        commentRenderIntersector.observe(comment.querySelector(`div[slot="commentMeta"]`));
    }

    css.addRule('.pp_muted_avatar { opacity: 0.5; }');
    css.addRule('.pp_muted_content { color: #a5a5a5; transition: color 0.2s; }');
    css.addRule('.pp_muted_content:hover { color: var(--color-tone-1); }');

    function renderComment(comment)
    {
        // collapse automoderator
        if(settings.isEnabled(SETTING_COLLAPSE_AUTOMODERATOR))
        {
            const author = comment.getAttribute(`author`);
            if(author != null && author == `AutoModerator`)
            {
                comment.setAttribute(`collapsed`, ``);
                return;
            }
        }

        // add anchors
        const nickname = comment.querySelector(`div[slot="commentMeta"]`).querySelector(`faceplate-hovercard[data-id="user-hover-card"]`);

        // skip [deleted]
        if(nickname == null) return;


        const tagsAnchor = document.createElement(`div`);
        tagsAnchor.setAttribute(`pp-anchor`, `tags`);
        nickname.parentNode.querySelector(`.ml-2xs`).after(tagsAnchor);

        const time = nickname.parentNode.querySelector(`time`).parentNode.parentNode;

        const infoAnchor = document.createElement(`div`);
        infoAnchor.setAttribute(`pp-anchor`, `info`);
        time.before(infoAnchor);

        // make ghosted when karma below zero
        if(settings.isEnabled(SETTING_GHOSTED_COMMENTS) && comment.getAttribute(`score`) < 0)
        {
            comment.querySelector(`div[slot="commentAvatar"]`).classList.add(`pp_muted_avatar`);
            comment.querySelector(`faceplate-tracker[noun="comment_author"]`).querySelector(`a`).style.color = `#a5a5a5`;
            comment.querySelector(`div[slot="comment"]`).classList.add(`pp_muted_content`);

        }

        // registry image
        const image = comment.querySelector(`div[slot="comment"]`).querySelector(`faceplate-img`);
        if(image != null)
        {
            registryImage(image);
        }


        renderCommentTags(comment);

        awaitContextMenu(comment);

        awaitUserInfo(comment);
    }

    css.addRule('.pp_imageViewable { cursor: pointer; }');

    function registryImage(imgContainer)
    {
        imgContainer.classList.add(`pp_imageViewable`);
        imgContainer.parentNode.removeAttribute(`href`);
        imgContainer.addEventListener(`click`, () => { imageViewer.open(imgContainer.getAttribute(`src`)); });
    }


    function renderCommentTags(comment)
    {
        if(settings.isDisabled(SETTING_USER_TAGS)) return;

        const userId = comment.getAttribute(`author`);
        if(userId == null) return;

        const tagsData = tags.get(userId);

        const tagsContainer = comment.querySelector(`div[pp-anchor="tags"]`);

        // skip not ininitialized comments
        if(tagsContainer == null) return;

        // clear old tags
        tagsContainer.parentNode.querySelectorAll(`svg[tag="true"]`).forEach(tag => { tag.remove(); });

        if(tagsData.tags != undefined)
        {
            for(const tag of tagsData.tags)
            {
                renderTag(tag);
            }
        }

        function renderTag(tag)
        {
            let tagSvg = buildSvg(20, 20, tagGraphics[tag], {w:1.5, f:`none`});
            tagSvg.setAttribute(`tag`, `true`);
            tagSvg.setAttribute(`viewBox`, `-4 -4 20 20`);
            tagSvg.style.color = tagConfigs[tag][TAG_CONFIG_COLOR];
            tagsContainer.after(tagSvg);
        }
    }

    function awaitUserInfo(comment)
    {
        const userId = comment.getAttribute(`author`);

        let userData = users.get(userId);

        if(userData.accountId == undefined)
        {
            loadUserInfo(comment, userId);
            return;
        }

        renderUserInfo(comment);
    }

    function loadUserInfo(comment, userId)
    {
        if(comment == null)
        {
            // do nothing
        }
        else if(loadingComments[userId] != undefined && loadingComments[userId] != null)
        {
            loadingComments[userId].push(comment);
            return;
        }
        else
        {
            loadingComments[userId] = [comment];
            currentLoads++;
        }


        fetch(`https://oauth.reddit.com/user/${userId}/about.json`, {
            cache: `no-cache`,
            method: `get`
        })
            .then(r => r.json())
            .then(result => {

            if(result != null)
            {
                let userData = users.get(userId);

                userData.rating = result.data.link_karma + result.data.comment_karma / 2;
                if(result.data.subreddit.title)
                {
                    userData.nick = result.data.subreddit.title;
                }
                userData.created = result.data.created;
                userData.accountId = result.kind + `_` + result.data.id;

                users.set(userId, userData);


                for (let comment of loadingComments[userId])
                {
                    renderUserInfo(comment);
                }

                loadingComments[userId] = null;
                currentLoads--;

                if(currentLoads == 0)
                {
                    loadingComments = {};
                }
            }
        });
    }

    // TODO: need replace vars in top
    function renderUserInfo(comment)
    {
        let nickName = comment.querySelector(`faceplate-tracker[noun="comment_author"]`).querySelector(`a`);

        const userId = comment.getAttribute(`author`);

        let userData = users.get(userId);


        if(settings.isEnabled(SETTING_SHOW_NAMES) && userData.nick != undefined && userData.nick)
        {
            nickName.textContent = userData.nick;
        }

        // skip render user info
        if(settings.isDisabled(SETTING_USER_INFO)) return;

        const infoAnchor = comment.querySelector(`div[pp-anchor="info"]`);


        const rating = document.createElement(`div`);
        rating.classList.add(`text-neutral-content-weak`, `text-12`);

        rating.textContent = (userData.rating < 10000) ? `${Math.round(userData.rating / 100) / 10}K` : `${Math.round(userData.rating / 1000)}K`;
        infoAnchor.after(rating);

        const point = document.createElement(`span`);
        point.classList.add(`inline-block`, `my-0`, `mx-2xs`, `text-12`, `text-neutral-content-weak`);
        point.textContent = `•`;
        rating.after(point);

        if(userData.created > (Date.now()/1000 - NEWUSER_SECONDS_SHIFT))
        {
            const tagsAnchor = comment.querySelector(`div[pp-anchor="tags"]`);

            const newSvg = buildSvg(20, 20, newUserGraphic);
            newSvg.setAttribute(`viewBox`, `-2 -2 20 20`);
            newSvg.style.color = `#69b508`;

            tagsAnchor.before(newSvg);
        }

    }


    function awaitContextMenu(comment)
    {
        let contextMenuButton = comment.querySelector(`shreddit-overflow-menu`)?.shadowRoot?.querySelector(`faceplate-dropdown-menu`);

        if(contextMenuButton == null)
        {
            setTimeout(() => { awaitContextMenu(comment); }, 50);
            return;
        }

        contextMenuButton.addEventListener(`click`, () => { renderContextMenu(comment); }, {once:true});
    }


    css.addRule('.pp_tagsPanel { display: flex; justify-content: space-around; width:auto; border-bottom: solid 1px var(--color-neutral-border-weak); padding: 4px; gap: 8px; margin-bottom: 4px; }');

    css.addRule('.pp_tagButton { cursor:pointer; display: flex; align-content: center; flex-wrap: wrap; height:45px; padding: 4px 20px; margin: 0px 0px; color: var(--color-neutral-border-weak); border-radius: 5px; }');
    css.addRule('.pp_tagButton svg { width:20px; transition: transform 0.15s; }');
    css.addRule('.pp_tagButton:hover svg { transform: scale(1.2, 1.2); transition: transform 0.3s; }');
    css.addRule('.pp_tagButton:hover { background-color: var(--color-neutral-background-hover);  }');
    css.addRule('.pp_tagButtonActive:hover { opacity: 0.8; }');

    css.addRule('.pp_tagHintOffset { left: 50%; position:absolute; }');
    css.addRule('.pp_tagHintContainer { display: flex; justify-content: center; position:fixed; }');
    css.addRule('.pp_tagHint { display: flex; align-items: center; position: absolute; top: -35px; height: 25px; padding: 0px 12px; color: var(--color-neutral-background-weak); font: var(--font-small); background-color: var(--color-neutral-content-strong); border-radius: 5px;}');



    function renderContextMenu(comment)
    {
        // close other context menu
        document.body.click();

        let contextMenuButton = comment.querySelector(`shreddit-overflow-menu`).shadowRoot;

        let originButton = contextMenuButton.querySelector(`faceplate-tracker[noun="report"]`);
        let contextMenuContainer = originButton.parentNode;

        if(settings.isEnabled(SETTING_HIDE_SHARE))
        {
            let linkButton = originButton.cloneNode(true);
            linkButton.querySelector(`span .text-14`).textContent = `Copy link`;
            originButton.before(linkButton);

            let linkPath = linkButton.querySelector(`path`);
            linkPath.setAttribute(`d`, linkGraphics[0].d);
            let linkPathB = linkPath.cloneNode(true);
            linkPathB.setAttribute(`d`, linkGraphics[1].d);
            linkPath.after(linkPathB);

            const permalink = comment.getAttribute(`permalink`);
            linkButton.addEventListener(`click`, () => {
                navigator.clipboard.writeText(`https://www.reddit.com${permalink}`);
                notify(`Link copied`);
            });
        }


        css.registry(contextMenuButton);

        // close context menu
        let openButton = contextMenuButton.querySelector(`button`);
        openButton.addEventListener(`click`, () => {document.body.click(); });


        // skip render tags
        if(settings.isDisabled(SETTING_USER_TAGS)) return;

        const tagHintOffset = document.createElement(`div`);
        tagHintOffset.classList.add(`pp_tagHintOffset`);
        contextMenuContainer.prepend(tagHintOffset);

        const tagHintContainer = document.createElement(`div`);
        tagHintContainer.classList.add(`pp_tagHintContainer`);
        tagHintOffset.prepend(tagHintContainer);

        const tagHint = AppendNew(tagHintContainer, `div`, `pp_tagHint`);
        tagHint.style.display = `none`;


        const tagsPanel = document.createElement(`div`);
        tagsPanel.classList.add(`pp_tagsPanel`);
        tagHintOffset.after(tagsPanel);

        const userId = comment.getAttribute(`author`);

        renderTagButton(USERTAG_FOLLOWED);
        renderTagButton(USERTAG_LIKED);
        renderTagButton(USERTAG_WARNING);
        renderTagButton(USERTAG_BLOCKED);

        function renderTagButton(tag)
        {
            const tagButton = AppendNew(tagsPanel, `span`, `pp_tagButton`);
            tagButton.setAttribute(`tag`, tag);

            const subscribeIcon = buildSvg(20, 20, tagButtonGraphics[tag], {w:2, f:`none`});
            tagButton.appendChild(subscribeIcon);

            tagButton.addEventListener(`click`, () => { tagButtonClick(userId, tag, tagButton); });
            tagButton.addEventListener(`mouseenter`, () => {tagHintEnter(userId, tag, tagButton, tagHint); });
            tagButton.addEventListener(`mouseleave`, () => {tagHintOut(userId, tag, tagButton, tagHint); });
        }

        refreshTagButtons(tagsPanel, userId);

    }

    function refreshTagButtons(tagsPanel, userId)
    {
        const tagsData = tags.get(userId);
        const tagsList = tagsData?.tags ?? [];

        tagsPanel.querySelectorAll(`.pp_tagButton`).forEach(button => {
            const tag = button.getAttribute(`tag`);

            button.removeAttribute(`has-cooldown`);
            button.removeAttribute(`has-blocked`);

            if(tagsList.includes(tag))
            {
                button.classList.add(`pp_tagButtonActive`);
                button.style.backgroundColor = tagConfigs[tag][TAG_CONFIG_COLOR];
                button.style.color = `white`;
            }
            else
            {
                button.classList.remove(`pp_tagButtonActive`);
                button.style.color = tagConfigs[tag][TAG_CONFIG_COLOR];
                button.style.removeProperty(`background-color`);

                if(tag == USERTAG_BLOCKED && tagsData.blockCooldown != undefined && Date.now()/1000 < tagsData.blockCooldown)
                {
                    button.setAttribute(`has-cooldown`, ``);
                    button.style.color = `#adadad`;
                }

                if(tag == USERTAG_FOLLOWED && tagsList.includes(USERTAG_BLOCKED))
                {
                    button.setAttribute(`has-blocked`, ``);
                    button.style.color = `#adadad`;
                }
            }
        });
    }


    function tagButtonClick(userId, tag, button)
    {
        if(button.getAttribute(`has-cooldown`) != null || button.getAttribute(`has-blocked`) != null)
        {
            notify(`Unable to do this`);
            return;
        }

        let tagsData = tags.get(userId);

        if(tagsData.tags == undefined)
        {
            tagsData.tags = [];
        }

        let isAdded = false;
        if(tagsData.tags.includes(tag))
        {
            tagsData.tags = tagsData.tags.filter(t => t != tag);
        }
        else
        {
            tagsData.tags.push(tag);
            isAdded = true;

            // auto clear follow state
            if(tag == USERTAG_BLOCKED)
            {
                tagsData.tags = tagsData.tags.filter(t => t != USERTAG_FOLLOWED);
            }
        }

        if(tagsData.tags.length > 1)
        {
            tagsData.tags.sort((firstItem, secondItem) => tagConfigs[firstItem][TAG_CONFIG_PRIORITY] - tagConfigs[secondItem][TAG_CONFIG_PRIORITY]);
        }

        tags.set(userId, tagsData);

        // refresh comments tags
        document.body.querySelectorAll(`shreddit-comment[author="${userId}"]`).forEach(comment => {
            renderCommentTags(comment);

            if(isAdded && tag == USERTAG_BLOCKED)
            {
                comment.setAttribute(`collapsed`, ``);
            }
        });

        // execute specific operations
        if(tag == USERTAG_FOLLOWED)
        {
            executeOperation(`UpdateProfileFollowState`, isAdded ? `FOLLOWED` : `NONE`, userId);
        }

        if(tag == USERTAG_BLOCKED)
        {
            if(!isAdded)
            {
                tagsData.blockCooldown = Date.now()/1000 + BLOCK_COOLDOWN_SECONDS;
                tags.set(userId, tagsData);
            }

            executeOperation(`UpdateRedditorBlockState`, isAdded ? `BLOCKED` : `NONE`, userId);
        }

        // refresh context menu
        refreshTagButtons(button.parentNode, userId);

    }

    function tagHintEnter(userId, tag, button, tagHint)
    {
        tagHint.style.display = null;

        tagHint.dataset.target = tag;

        const tagsData = tags.get(userId);

        tagHint.innerText = tagConfigs[tag][(tagsData?.tags ?? []).includes(tag) ? TAG_CONFIG_HINT_ACTIVATED : TAG_CONFIG_HINT];

        if(button.getAttribute(`has-cooldown`) != null)
        {
            const cooldownHours = Math.round((tagsData.blockCooldown - Date.now()/1000) / HOUR_SECONDS);
            tagHint.innerText = `Unable block for ${cooldownHours}h after unblocking`;
        }

        if(button.getAttribute(`has-blocked`) != null)
        {
            tagHint.innerText = `Unable to follow on blocked user`;
        }
    }

    function tagHintOut(userId, tag, button, tagHint)
    {
        if(tagHint.dataset?.target == tag)
        {
            tagHint.style.display = `none`;
        }
    }


    function executeOperation(operation, state, userId)
    {
        let userData = users.get(userId);

        const body = {
            csrf_token:getCookie(`csrf_token`),
            operation:operation,
            variables:{
                input: operation == `UpdateRedditorBlockState` ? {
                    redditorId:userData.accountId,
                    blockState:state
                } : {
                    accountId:userData.accountId,
                    state:state,
                }
            }
        };

        fetch(`https://www.reddit.com/svc/shreddit/graphql`, {
            method: `post`,
            headers: new Headers({
                'Accept': `application/json`,
                'Content-Type': `application/json`
            }),
            body: JSON.stringify(body)
        })
            .then(r => r.json())
            .then(result => {

            if(result != null && result.errors?.message)
            {
                notify(result.errors.message);
            }

        });
    }







    // ***********************************************************************************************************************
    // *********************************************** SETTINGS **************************************************************
    // ***********************************************************************************************************************

    css.addRule(' .pp_SettingSubtittle { font-size: 10px; font-weight: 700; letter-spacing: 0.5px; line-height: 12px; min-height: 20px; color: #7c7c7c; text-transform: uppercase; border-bottom: 1px solid #edeff1;  margin-top: 1rem; padding: 0rem 3rem;}');


    css.addRule(' .pp_SettingPropertyHeader { display: flex; flex-direction: column; margin-left: 3rem; justify-content: center; }');

    css.addRule(' .pp_SettingPropertyHeaderTittle { font-size: 16px; font-weight: 500; line-height: 20px; margin-bottom: 4px;  }');
    css.addRule(' .pp_SettingPropertyHeaderDescription { font-size: 12px; font-weight: 400; line-height: 16px; color: #7c7c7c; }');
    css.addRule(' .pp_SettingPropertyButtonContainer { display: flex; align-items: center; justify-content:flex-end; flex-grow:1; }');


    css.addRule(' .pp_SettingChangesBannerContainer { position: absolute; top: 0px; width: 900px; overflow-y: hidden; opacity: 0; transition: opacity 0.15s ease-in-out; }');
    css.addRule(' .pp_SettingChangesBanner { display: flex; justify-content: center;  margin: 2rem 15%; padding: 1rem; border-radius: 15px; background-color: #ffd40017; border: solid 1px #ffd400; color: #d7b300; font-weight: 500; }');
    css.addRule(' .pp_SettingChangesBannerActive { opacity: 1 !important; }');

    function renderSettingsWindow(win, context)
    {
        // close user context menu
        document.body.click();

        win.changes = 0;


        const changesBannerContainer = AppendNew(win.content, `div`, `pp_SettingChangesBannerContainer`);
        const changesBanner = AppendNew(changesBannerContainer, `div`, `pp_SettingChangesBanner`);
        changesBanner.textContent = `Page will be reloaded to apply new settings`;


        const scroll = AppendNew(win.content, `div`, [`pp_WindowScrollContent`, `styled-scrollbars`]);

        const elements = AppendNew(scroll, `div`, `pp_WindowElementsContainer`);


        addSettingToggle(`Wide mode`, `Fit sidebars to screen borders and limit content width to 700px`, SETTING_WIDE_MODE);

        addSubtittle(`Left sidebar`);

        addSettingToggle(`Show Custom feeds`, null, SETTING_SIDEBAR_CUSTOMS);
        addSettingToggle(`Show Recent`, null, SETTING_SIDEBAR_RECENT);
        addSettingToggle(`Show Communities`, null, SETTING_SIDEBAR_SUBS);
        addSettingToggle(`Show Resources`, null, SETTING_SIDEBAR_RESOURCES);

        addSubtittle(`Feed`);

        addSettingToggle(`Feed buttons`, `Unwrap feed sorting buttons`, SETTING_FEED_BUTTONS);
        addSettingToggle(`Flair bar`, `Display available flairs to faster navigation. Specific flairs may be hidden in subreddit flairs settings.`, SETTING_FLAIR_BAR);

        addSettingToggle(`Soft backplates`, `Make post backplates with gradient color`, SETTING_BACKPLATES);

        addSubtittle(`Comments`);

        addSettingToggle(`User info`, `Show user karma and leaf mark for new user`, SETTING_USER_INFO);
        addSettingToggle(`User tags`, `Enable custom tags for users`, SETTING_USER_TAGS);

        addSettingToggle(`Show names`, `Use display names (if user set it) instead usernames`, SETTING_SHOW_NAMES);
        addSettingToggle(`Bigger fonts`, `Make fonts bigger for better reading`, SETTING_BIGGER_FONTS);
        addSettingToggle(`Hide share button`, `Replace share button to context menu`, SETTING_HIDE_SHARE);
        addSettingToggle(`Ghosted comments`, `Make comments ghosted when karma below zero`, SETTING_GHOSTED_COMMENTS);
        addSettingToggle(`Collapse Automoderator`, `Automatic collapse all automoderator comments`, SETTING_COLLAPSE_AUTOMODERATOR);


        function addSubtittle(text)
        {
            const subtittle = AppendNew(elements, `h3`, `pp_SettingSubtittle`);
            subtittle.textContent = text;
        }

        function addSettingToggle(tittleText, descriptionText, settingName)
        {
            const propertyArea = AppendNew(elements, `div`, `pp_WindowElement`);

            const header = AppendNew(propertyArea, `div`, `pp_SettingPropertyHeader`);
            const tittle = AppendNew(header, `div`, `pp_SettingPropertyHeaderTittle`);
            tittle.textContent = tittleText;
            if(descriptionText != null)
            {
                const description = AppendNew(header, `div`, `pp_SettingPropertyHeaderDescription`);
                description.textContent = descriptionText;
            }

            const buttonContainer = AppendNew(propertyArea, `div`, `pp_SettingPropertyButtonContainer`);

            const toggleArea = AppendNew(buttonContainer, `div`, `pp_fs_flairPanelCheckboxArea`);
            const toggleContainer = AppendNew(toggleArea, `div`, `pp_checkBoxContainer`);
            const toggleBack = AppendNew(toggleContainer, `button`, `pp_checkBoxButton`);
            toggleBack.classList.toggle(`pp_checkBoxButtonActive`, settings.get(settingName) != false);
            const knob = AppendNew(toggleBack, `div`, `pp_checkBoxKnob`);

            let changed = false;

            toggleBack.addEventListener(`click`, e => {

                let state = !(settings.get(settingName) == false);

                toggleBack.classList.toggle(`pp_checkBoxButtonActive`, !state);

                settings.set(settingName, !state);

                win.changes += changed ? -1 : 1;
                changed = !changed;

                changesBannerContainer.classList.toggle(`pp_SettingChangesBannerActive`, win.changes > 0);
            });
        }

    }





    // ***********************************************************************************************************************
    // ************************************************* UTILS ***************************************************************
    // ***********************************************************************************************************************

    function getCurrentSub()
    {
        const raw = window.location.href.split(`reddit.com/r/`);
        return raw.length > 1 ? raw[1].split(`/`)[0] : null;
    }

    function checkIsRendered(node)
    {
        if(node.getAttribute(`pp-rendered`) != null)
        {
            return true;
        }
        else
        {
            node.setAttribute(`pp-rendered`, ``);
            return false;
        }
    }

    function notify(message, actionTittle, actionCallback)
    {
        const hasAction = actionTittle != undefined;

        let toaster = document.body?.querySelector(`alert-controller`)?.shadowRoot?.querySelector(`toaster-lite`);

        if(toaster == null)
        {
            console.log(`Reddit++: Failed to notify (${message})`);
            return;
        }

        let toast = document.createElement(`faceplate-toast`);
        toast.classList.add(`theme-rpl`);
        toast.textContent = message;

        toaster.appendChild(toast);

        setTimeout(() => {toast.setAttribute(`_fading`, ``); }, hasAction ? 8000 : 3000);
    }


    function buildSvg(w, h, graphics, settings)
    {
        const XMLNS = `http://www.w3.org/2000/svg`;
        const svg = document.createElementNS(XMLNS, `svg`);
        svg.setAttribute(`viewBox`, `0 0 ${w} ${h}`);
        svg.setAttribute(`width`, `${w}px`);
        svg.setAttribute( `height`, `${h}px`);
        svg.setAttribute(`fill`, `none`);

        for(var graphic of graphics)
        {
            let path = document.createElementNS(XMLNS, `path`);
            path.setAttribute(`stroke-linecap`, `round`);
            path.setAttribute(`stroke-linejoin`, `round`);
            path.setAttribute(`stroke`, graphic?.c ?? (settings?.c ?? `currentColor`));
            path.setAttribute(`stroke-width`, graphic?.w ?? (settings?.w ?? 1));
            path.setAttribute(`fill`, graphic?.f ?? (settings?.f ?? `currentColor`));
            path.setAttribute(`d`, graphic.d);
            svg.appendChild(path);
        }

        return svg;
    }

    function AppendNew(prev, name, classes)
    {
        const el = document.createElement(name);

        if(classes != undefined)
        {
            if(typeof classes === `string` && classes)
            {
                el.classList.add(classes);
            }
            else
            {
                for(const c of classes)
                {
                    el.classList.add(c);
                }
            }
        }

        prev.append(el);
        return el;
    }

    function getSettingsRevision()
    {
        return localStorage.getItem(`pp_settings_revision`) ?? 0;
    }

    function nextSettingsRevision()
    {
        settingsRevision++;
        localStorage.setItem(`pp_settings_revision`, settingsRevision);
    }

    function getCookie(key)
    {
        return document.cookie
            .split(`; `)
            .find((row) => row.startsWith(key))
            ?.split(`=`)[1];
    }

    function awaitElement(element, callback)
    {
        if(element == null)
        {
            setTimeout(() => { callback(document.body); }, 10);
            return true;
        }

        return false;
    }


})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址